Bug 1009816 - Firefox desktop: openh264 updates: check, download, install. r=rstrong, gfritzsche
authorBrian R. Bondy <netzen@gmail.com>
Thu, 17 Jul 2014 21:46:10 -0400
changeset 208891 8ada22bb1f40543b63d3a216e29b36c924ad8b58
parent 208890 51b6fd0998d81dcd0e9b1fe0e41e75375d451ba5
child 208892 ece3f69eb1b1b865347bfc115ee93e969192d64c
push idunknown
push userunknown
push dateunknown
reviewersrstrong, gfritzsche
bugs1009816
milestone33.0a1
Bug 1009816 - Firefox desktop: openh264 updates: check, download, install. r=rstrong, gfritzsche
browser/app/profile/firefox.js
browser/base/content/browser.js
testing/profiles/prefs_general.js
toolkit/modules/GMPInstallManager.jsm
toolkit/modules/moz.build
toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -134,16 +134,18 @@ pref("app.update.cert.maxErrors", 5);
 //    the value for the name must be the same as the value for the attribute name
 //    on the certificate.
 // If these conditions aren't met it will be treated the same as when there is
 // no update available. This validation will not be performed when the
 // |app.update.url.override| user preference has been set for testing updates or
 // when the |app.update.cert.checkAttributes| preference is set to false. Also,
 // the |app.update.url.override| preference should ONLY be used for testing.
 // IMPORTANT! metro.js should also be updated for updates to certs.X.issuerName
+// IMPORTANT! media.gmp-manager.certs.* prefs should also be updated if these
+// are updated.
 
 // Non-release builds (Nightly, Aurora, etc.) have been switched over to aus4.mozilla.org.
 // This condition protects us against accidentally using it for release builds.
 #ifndef RELEASE_BUILD
 pref("app.update.certs.1.issuerName", "CN=DigiCert Secure Server CA,O=DigiCert Inc,C=US");
 pref("app.update.certs.1.commonName", "aus4.mozilla.org");
 
 pref("app.update.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
@@ -1585,16 +1587,55 @@ pref("identity.fxaccounts.settings.uri",
 // On GTK, we now default to showing the menubar only when alt is pressed:
 #ifdef MOZ_WIDGET_GTK
 pref("ui.key.menuAccessKeyFocuses", true);
 #endif
 
 // Encrypted media extensions.
 pref("media.eme.enabled", false);
 
+// GMPInstallManager prefs
+
+// Enables some extra logging (can reduce performance)
+pref("media.gmp-manager.log", false);
+
+// User-settable override to media.gmp-manager.url for testing purposes.
+//pref("media.gmp-manager.url.override", "");
+
+// Update service URL for GMP install/updates:
+pref("media.gmp-manager.url", "https://aus4.mozilla.org/update/3/GMP/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
+
+// When |media.gmp-manager.cert.requireBuiltIn| is true or not specified the
+// final certificate and all certificates the connection is redirected to before
+// the final certificate for the url specified in the |media.gmp-manager.url|
+// preference must be built-in.
+pref("media.gmp-manager.cert.requireBuiltIn", true);
+
+// The |media.gmp-manager.certs.| preference branch contains branches that are
+// sequentially numbered starting at 1 that contain attribute name / value
+// pairs for the certificate used by the server that hosts the update xml file
+// as specified in the |media.gmp-manager.url| preference. When these preferences are
+// present the following conditions apply for a successful update check:
+// 1. the uri scheme must be https
+// 2. the preference name must exist as an attribute name on the certificate and
+//    the value for the name must be the same as the value for the attribute name
+//    on the certificate.
+// If these conditions aren't met it will be treated the same as when there is
+// no update available. This validation will not be performed when the
+// |media.gmp-manager.url.override| user preference has been set for testing updates or
+// when the |media.gmp-manager.cert.checkAttributes| preference is set to false. Also,
+// the |media.gmp-manager.url.override| preference should ONLY be used for testing.
+// IMPORTANT! app.update.certs.* prefs should also be updated if these
+// are updated.
+pref("media.gmp-manager.cert.checkAttributes", true);
+pref("media.gmp-manager.certs.1.issuerName", "CN=DigiCert Secure Server CA,O=DigiCert Inc,C=US");
+pref("media.gmp-manager.certs.1.commonName", "aus4.mozilla.org");
+pref("media.gmp-manager.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
+pref("media.gmp-manager.certs.2.commonName", "aus4.mozilla.org");
+
 // Delete HTTP cache v2 data of users that didn't opt-in manually
 pref("browser.cache.auto_delete_cache_version", 1);
 // Play with different values of the decay time and get telemetry,
 // 0 means to randomize (and persist) the experiment value in users' profiles,
 // -1 means no experiment is run and we use the preferred value for frecency (6h)
 pref("browser.cache.frecency_experiment", 0);
 
 pref("browser.translation.detectLanguage", false);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -16,16 +16,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
+                                  "resource://gre/modules/GMPInstallManager.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
@@ -1311,16 +1313,24 @@ var gBrowserInit = {
       Cu.reportError("Could not end startup crash tracking: " + ex);
     }
 
     if (typeof WindowsPrefSync !== 'undefined') {
       // Pulls in Metro controlled prefs and pushes out Desktop controlled prefs
       WindowsPrefSync.init();
     }
 
+    // Delay this a minute because there's no rush
+    setTimeout(() => {
+      this.gmpInstallManager = new GMPInstallManager();
+      // We don't really care about the results, if somenoe is interested they
+      // can check the log.
+      this.gmpInstallManager.simpleCheckAndInstall();
+    }, 1000 * 60);
+
     SessionStore.promiseInitialized.then(() => {
       // Bail out if the window has been closed in the meantime.
       if (window.closed) {
         return;
       }
 
       // Enable the Restore Last Session command if needed
       RestoreLastSessionObserver.init();
@@ -1460,16 +1470,19 @@ var gBrowserInit = {
         gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton);
       } catch (ex) {
         Cu.reportError(ex);
       }
 
       if (typeof WindowsPrefSync !== 'undefined') {
         WindowsPrefSync.uninit();
       }
+      if (this.gmpInstallManager) {
+        this.gmpInstallManager.uninit();
+      }
 
       BrowserOffline.uninit();
       OfflineApps.uninit();
       IndexedDBPromptHelper.uninit();
       LightweightThemeListener.uninit();
       PanelUI.uninit();
     }
 
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -35,16 +35,18 @@ user_pref("dom.min_background_timeout_va
 user_pref("test.mousescroll", true);
 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
 user_pref("network.http.prompt-temp-redirect", false);
 user_pref("media.cache_size", 100);
 user_pref("media.volume_scale", "0.01");
 user_pref("security.warn_viewing_mixed", false);
 user_pref("app.update.enabled", false);
 user_pref("app.update.staging.enabled", false);
+// Make sure GMPInstallManager won't hit the network.
+user_pref("media.gmp-manager.url", "https://%(server)s/dummy.xml");
 user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
 user_pref("dom.w3c_touch_events.enabled", 1);
 user_pref("dom.undo_manager.enabled", true);
 user_pref("dom.webcomponents.enabled", true);
 user_pref("dom.animations-api.core.enabled", true);
 // Set a future policy version to avoid the telemetry prompt.
 user_pref("toolkit.telemetry.prompted", 999);
 user_pref("toolkit.telemetry.notifiedOptOut", 999);
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/GMPInstallManager.jsm
@@ -0,0 +1,951 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} =
+  Components;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL  = 0;
+// 1 day default
+const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
+const OPEN_H264_ID = "openh264-plugin@cisco.com";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/ctypes.jsm");
+
+this.EXPORTED_SYMBOLS = ["GMPInstallManager", "GMPExtractor", "GMPDownloader",
+                         "GMPAddon", "GMPPrefs"];
+
+var gLocale = null;
+
+// Shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
+  let temp = { };
+  Cu.import("resource://gre/modules/CertUtils.jsm", temp);
+  return temp;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
+                                  "resource://gre/modules/UpdateChannel.jsm");
+
+// Used to determine if logging should be enabled
+XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function() {
+  return GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED);
+});
+
+
+function getScopedLogger(prefix) {
+  let logger = Log.repository.getLogger(prefix);
+  if (gLogEnabled) {
+    logger.level = Log.Level.Debug;
+    let appender = new Log.DumpAppender();
+    logger.addAppender(appender);
+  }
+  return logger;
+}
+
+
+/**
+ * Manages preferences for GMP addons
+ */
+let GMPPrefs = {
+  /**
+   * Obtains the specified preference in relation to the specified addon
+   * @param key The GMPPrefs key value to use
+   * @param addon The addon to scope the preference to
+   * @param defaultValue The default value if no preference exists
+   * @return The obtained preference value, or the defaultVlaue if none exists
+   */
+  get: function(key, addon, defaultValue) {
+    if (key === GMPPrefs.KEY_APP_DISTRIBUTION ||
+        key === GMPPrefs.KEY_APP_DISTRIBUTION_VERSION) {
+      let prefValue = "default";
+      try {
+        prefValue = Services.prefs.getDefaultBranch(null).getCharPref(key);
+      } catch (e) {
+        // use default when pref not found
+      }
+      return prefValue;
+    }
+
+    return Preferences.get(this._getPrefKey(key, addon), defaultValue);
+  },
+  /**
+   * Sets the specified preference in relation to the specified addon
+   * @param key The GMPPrefs key value to use
+   * @param val The value to set
+   * @param addon The addon to scope the preference to
+   */
+  set: function(key, val, addon) {
+    let log = getScopedLogger("GMPPrefs.set");
+    log.info("Setting pref: " + this._getPrefKey(key, addon) +
+             " to value: " + val);
+    return Preferences.set(this._getPrefKey(key, addon), val);
+  },
+  _getPrefKey: function(key, addon) {
+    return  key.replace("{0}", addon || "");
+  },
+
+  /**
+   * List of keys which can be used in get and set
+   */
+  KEY_LOG_ENABLED: "media.gmp-manager.log",
+  KEY_ADDON_LAST_UPDATE: "media.{0}.lastUpdate",
+  KEY_ADDON_PATH: "media.{0}.path",
+  KEY_ADDON_VERSION: "media.{0}.version",
+  KEY_URL: "media.gmp-manager.url",
+  KEY_URL_OVERRIDE: "media.gmp-manager.url.override",
+  KEY_CERT_CHECKATTRS: "media.gmp-manager.cert.checkAttributes",
+  KEY_CERT_REQUIREBUILTIN: "media.gmp-manager.cert.requireBuiltIn",
+  KEY_UPDATE_LAST_CHECK: "media.gmp-manager.lastCheck",
+  KEY_UPDATE_SECONDS_BETWEEN_CHECKS: "media.gmp-manager.secondsBetweenChecks",
+  KEY_APP_DISTRIBUTION: "distribution.id",
+  KEY_APP_DISTRIBUTION_VERSION: "distribution.version",
+
+  CERTS_BRANCH: "media.gmp-manager.certs."
+};
+
+// This is copied directly from nsUpdateService.js
+// It is used for calculating the URL string w/ var replacement.
+// TODO: refactor this out somewhere else
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function aus_gOSVersion() {
+  let osVersion;
+  let sysInfo = Cc["@mozilla.org/system-info;1"].
+                getService(Ci.nsIPropertyBag2);
+  try {
+    osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+  }
+  catch (e) {
+    LOG("gOSVersion - OS Version unknown: updates are not possible.");
+  }
+
+  if (osVersion) {
+#ifdef XP_WIN
+    const BYTE = ctypes.uint8_t;
+    const WORD = ctypes.uint16_t;
+    const DWORD = ctypes.uint32_t;
+    const WCHAR = ctypes.jschar;
+    const BOOL = ctypes.int;
+
+    // This structure is described at:
+    // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+    const SZCSDVERSIONLENGTH = 128;
+    const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+        [
+        {dwOSVersionInfoSize: DWORD},
+        {dwMajorVersion: DWORD},
+        {dwMinorVersion: DWORD},
+        {dwBuildNumber: DWORD},
+        {dwPlatformId: DWORD},
+        {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+        {wServicePackMajor: WORD},
+        {wServicePackMinor: WORD},
+        {wSuiteMask: WORD},
+        {wProductType: BYTE},
+        {wReserved: BYTE}
+        ]);
+
+    // This structure is described at:
+    // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
+    const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO',
+        [
+        {wProcessorArchitecture: WORD},
+        {wReserved: WORD},
+        {dwPageSize: DWORD},
+        {lpMinimumApplicationAddress: ctypes.voidptr_t},
+        {lpMaximumApplicationAddress: ctypes.voidptr_t},
+        {dwActiveProcessorMask: DWORD.ptr},
+        {dwNumberOfProcessors: DWORD},
+        {dwProcessorType: DWORD},
+        {dwAllocationGranularity: DWORD},
+        {wProcessorLevel: WORD},
+        {wProcessorRevision: WORD}
+        ]);
+
+    let kernel32 = false;
+    try {
+      kernel32 = ctypes.open("Kernel32");
+    } catch (e) {
+      LOG("gOSVersion - Unable to open kernel32! " + e);
+      osVersion += ".unknown (unknown)";
+    }
+
+    if(kernel32) {
+      try {
+        // Get Service pack info
+        try {
+          let GetVersionEx = kernel32.declare("GetVersionExW",
+                                              ctypes.default_abi,
+                                              BOOL,
+                                              OSVERSIONINFOEXW.ptr);
+          let winVer = OSVERSIONINFOEXW();
+          winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+          if(0 !== GetVersionEx(winVer.address())) {
+            osVersion += "." + winVer.wServicePackMajor
+                      +  "." + winVer.wServicePackMinor;
+          } else {
+            LOG("gOSVersion - Unknown failure in GetVersionEX (returned 0)");
+            osVersion += ".unknown";
+          }
+        } catch (e) {
+          LOG("gOSVersion - error getting service pack information. Exception: " + e);
+          osVersion += ".unknown";
+        }
+
+        // Get processor architecture
+        let arch = "unknown";
+        try {
+          let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
+                                                     ctypes.default_abi,
+                                                     ctypes.void_t,
+                                                     SYSTEM_INFO.ptr);
+          let sysInfo = SYSTEM_INFO();
+          // Default to unknown
+          sysInfo.wProcessorArchitecture = 0xffff;
+
+          GetNativeSystemInfo(sysInfo.address());
+          switch(sysInfo.wProcessorArchitecture) {
+            case 9:
+              arch = "x64";
+              break;
+            case 6:
+              arch = "IA64";
+              break;
+            case 0:
+              arch = "x86";
+              break;
+          }
+        } catch (e) {
+          LOG("gOSVersion - error getting processor architecture.  Exception: " + e);
+        } finally {
+          osVersion += " (" + arch + ")";
+        }
+      } finally {
+        kernel32.close();
+      }
+    }
+#endif
+
+    try {
+      osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+    }
+    catch (e) {
+      // Not all platforms have a secondary widget library, so an error is nothing to worry about.
+    }
+    osVersion = encodeURIComponent(osVersion);
+  }
+  return osVersion;
+});
+
+// This is copied directly from nsUpdateService.js
+// It is used for calculating the URL string w/ var replacement.
+// TODO: refactor this out somewhere else
+XPCOMUtils.defineLazyGetter(this, "gABI", function aus_gABI() {
+  let abi = null;
+  try {
+    abi = Services.appinfo.XPCOMABI;
+  }
+  catch (e) {
+    LOG("gABI - XPCOM ABI unknown: updates are not possible.");
+  }
+#ifdef XP_MACOSX
+  // Mac universal build should report a different ABI than either macppc
+  // or mactel.
+  let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+                 getService(Ci.nsIMacUtils);
+
+  if (macutils.isUniversalBinary)
+    abi += "-u-" + macutils.architecturesInBinary;
+#ifdef MOZ_SHARK
+  // Disambiguate optimised and shark nightlies
+  abi += "-shark"
+#endif
+#endif
+  return abi;
+});
+
+/**
+ * Provides an easy API for downloading and installing GMP Addons
+ */
+function GMPInstallManager() {
+}
+/**
+ * Temp file name used for downloading
+ */
+GMPInstallManager.prototype = {
+  /**
+   * Obtains a URL with replacement of vars
+   */
+  _getURL: function() {
+    let log = getScopedLogger("GMPInstallManager._getURL");
+    // Use the override URL if it is specified.  The override URL is just like
+    // the normal URL but it does not check the cert.
+    let url = GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE);
+    if (url) {
+      log.info("Using override url: " + url);
+    } else {
+      url = GMPPrefs.get(GMPPrefs.KEY_URL);
+      log.info("Using url: " + url);
+    }
+
+    url =
+      url.replace(/%PRODUCT%/g, Services.appinfo.name)
+         .replace(/%VERSION%/g, Services.appinfo.version)
+         .replace(/%BUILD_ID%/g, Services.appinfo.appBuildID)
+         .replace(/%BUILD_TARGET%/g, Services.appinfo.OS + "_" + gABI)
+         .replace(/%OS_VERSION%/g, gOSVersion);
+    if (/%LOCALE%/.test(url)) {
+      // TODO: Get the real local, does it actually matter for GMP plugins?
+      url = url.replace(/%LOCALE%/g, "en-US");
+    }
+    url =
+      url.replace(/%CHANNEL%/g, UpdateChannel.get())
+         .replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion)
+         .replace(/%DISTRIBUTION%/g,
+                  GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION))
+         .replace(/%DISTRIBUTION_VERSION%/g,
+                  GMPPrefs.get(GMPPrefs.KEY_APP_DISTRIBUTION_VERSION))
+         .replace(/\+/g, "%2B");
+    log.info("Using url (with replacement): " + url);
+    return url;
+  },
+  /**
+   * Performs an addon check.
+   * @return a promise which will be resolved or rejected.
+   *         The promise is resolved with an array of GMPAddons
+   *         The promise is rejected with an object with properties:
+   *           target: The XHR request object
+   *           status: The HTTP status code
+   *           type: Sometimes specifies type of rejection
+   */
+  checkForAddons: function() {
+    let log = getScopedLogger("GMPInstallManager.checkForAddons");
+    if (this._deferred) {
+        log.error("checkForAddons already called");
+        return Promise.reject({type: "alreadycalled"});
+    }
+    this._deferred = Promise.defer();
+    let url = this._getURL();
+
+    this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+                    createInstance(Ci.nsISupports);
+    // This is here to let unit test code override XHR
+    if (this._request.wrappedJSObject) {
+      this._request = this._request.wrappedJSObject;
+    }
+    this._request.open("GET", url, true);
+    let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS,
+                                        undefined, true);
+    this._request.channel.notificationCallbacks =
+      new gCertUtils.BadCertHandler(allowNonBuiltIn);
+    // Prevent the request from reading from the cache.
+    this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+    // Prevent the request from writing to the cache.
+    this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+
+    this._request.overrideMimeType("text/xml");
+    // The Cache-Control header is only interpreted by proxies and the
+    // final destination. It does not help if a resource is already
+    // cached locally.
+    this._request.setRequestHeader("Cache-Control", "no-cache");
+    // HTTP/1.0 servers might not implement Cache-Control and
+    // might only implement Pragma: no-cache
+    this._request.setRequestHeader("Pragma", "no-cache");
+
+    this._request.addEventListener("error", this.onErrorXML.bind(this) ,false);
+    this._request.addEventListener("load", this.onLoadXML.bind(this), false);
+
+    log.info("sending request to: " + url);
+    this._request.send(null);
+
+    return this._deferred.promise;
+  },
+  /**
+   * Installs the specified addon and calls a callback when done.
+   * @param gmpAddon The GMPAddon object to install
+   * @return a promise which will be resolved or rejected
+   *         The promise will resolve with an array of paths that were extracted
+   *         The promise will reject with an error object:
+   *           target: The XHR request object
+   *           status: The HTTP status code
+   *           type: A string to represent the type of error
+   *                 downloaderr, or verifyerr
+   */
+  installAddon: function(gmpAddon) {
+    if (this._deferred) {
+        log.error("checkForAddons already called");
+        return Promise.reject({type: "alreadycalled"});
+    }
+    this.gmpDownloader = new GMPDownloader(gmpAddon);
+    return this.gmpDownloader.start();
+  },
+  _getTimeSinceLastCheck: function() {
+    let now = Math.round(Date.now() / 1000);
+    // Default to 0 here because `now - 0` will be returned later if that case
+    // is hit. We want a large value so a check will occur.
+    let lastCheck = GMPPrefs.get(GMPPrefs.KEY_UPDATE_LAST_CHECK,
+                                 undefined, 0);
+    // Handle clock jumps, return now since we want it to represent
+    // a lot of time has passed since the last check.
+    if (now < lastCheck) {
+      return now;
+    }
+    return now - lastCheck;
+  },
+  _updateLastCheck: function() {
+    let now = Math.round(Date.now() / 1000);
+    GMPPrefs.set(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
+  },
+  /**
+   * Wrapper for checkForAddons and installaddon.
+   * Will only install if not already installed and will log the results.
+   */
+  simpleCheckAndInstall: function() {
+    let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
+    let secondsBetweenChecks =
+      GMPPrefs.get(GMPPrefs.KEY_UPDATE_SECONDS_BETWEEN_CHECKS, undefined,
+                   DEFAULT_SECONDS_BETWEEN_CHECKS)
+    let secondsSinceLast = this._getTimeSinceLastCheck();
+    log.info("Last check was: " + secondsSinceLast +
+             " seconds ago, minimum seconds: " + secondsBetweenChecks);
+    if (secondsBetweenChecks > secondsSinceLast) {
+      log.info("Will not check for updates.");
+      return Promise.resolve();
+    }
+
+    let promise = this.checkForAddons();
+    promise.then(gmpAddons => {
+      this._updateLastCheck();
+      log.info("Found " + gmpAddons.length + " addons advertised.");
+      let addonsToInstall = gmpAddons.filter(gmpAddon => {
+        log.info("Found addon: " + gmpAddon.toString());
+        return gmpAddon.isValid && gmpAddon.isOpenH264 &&
+               !gmpAddon.isInstalled
+      });
+      if (!addonsToInstall.length) {
+        log.info("No new addons to install, returning");
+        return;
+      }
+      addonsToInstall.forEach(gmpAddon => {
+        promise = this.installAddon(gmpAddon);
+        promise.then(extractedPaths => {
+          // installed!
+          log.info("Addon installed successfully: " + gmpAddon.toString());
+        }, () => {
+          if (!GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED)) {
+            Cu.reportError(gmpAddon.toString() + " could not be installed. Enable " +
+                           GMPPrefs.KEY_LOG_ENABLED + " for details!");
+          }
+          log.error("Could not install addon: " + gmpAddon.toString());
+        });
+      });
+    });
+  },
+
+  /**
+   * Makes sure everything is cleaned up
+   */
+  uninit: function() {
+    let log = getScopedLogger("GMPDownloader.uninit");
+    if (this._request) {
+      log.info("Aborting request");
+      this._request.abort();
+    }
+    if (this._deferred) {
+        log.info("Rejecting deferred");
+        this._deferred.reject({type: "uninitialized"});
+    }
+    log.info("Done cleanup");
+  },
+
+  /**
+   * If set to true, specifies to leave the temporary downloaded zip file.
+   * This is useful for tests.
+   */
+  overrideLeaveDownloadedZip: false,
+
+  /**
+   * The XMLHttpRequest succeeded and the document was loaded.
+   * @param event The nsIDOMEvent for the load
+  */
+  onLoadXML: function(event) {
+    let log = getScopedLogger("GMPInstallManager.onLoadXML");
+    log.info("request completed downloading document");
+
+    let certs = null;
+    if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
+        GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS, undefined, true)) {
+      certs = gCertUtils.readCertPrefs(GMPPrefs.CERTS_BRANCH);
+    }
+
+    let allowNonBuiltIn = !GMPPrefs.get(GMPPrefs.KEY_CERT_REQUIREBUILTIN,
+                                        undefined, true);
+    log.info("allowNonBuiltIn: " + allowNonBuiltIn);
+    gCertUtils.checkCert(this._request.channel, allowNonBuiltIn, certs);
+
+    this.parseResponseXML();
+  },
+
+  /**
+   * Returns the status code for the XMLHttpRequest
+   */
+  _getChannelStatus: function(request) {
+    let log = getScopedLogger("GMPInstallManager._getChannelStatus");
+    let status = 0;
+    try {
+      status = request.status;
+      log.info("request.status is: " + request.status);
+    }
+    catch (e) {
+    }
+
+    if (status == 0) {
+      status = request.channel.QueryInterface(Ci.nsIRequest).status;
+    }
+    return status;
+  },
+
+  /**
+   * There was an error of some kind during the XMLHttpRequest
+   * @param event The nsIDOMEvent for the error
+  */
+  onErrorXML: function(event) {
+    let log = getScopedLogger("GMPInstallManager.onErrorXML");
+    let request = event.target;
+    let status = this._getChannelStatus(request);
+    let message = "request.status: " + status;
+    log.warn(message);
+    this._deferred.reject({
+      target: request,
+      status: status,
+      message: message
+    });
+    delete this._deferred;
+  },
+
+  /**
+   * Returns an array of GMPAddon objects discovered by the update check.
+   * Or returns an empty array if there were any problems with parsing.
+   * If there's an error, it will be logged if logging is enabled.
+   */
+  parseResponseXML: function() {
+    try {
+      let log = getScopedLogger("GMPInstallManager.parseResponseXML");
+      let updatesElement = this._request.responseXML.documentElement;
+      if (!updatesElement) {
+        let message = "empty updates document";
+        log.warn(message);
+        this._deferred.reject({
+          target: this._request,
+          message: message
+        });
+        delete this._deferred;
+        return;
+      }
+
+      if (updatesElement.nodeName != "updates") {
+        let message = "got node name: " + updatesElement.nodeName +
+          ", expected: updates";
+        log.warn(message);
+        this._deferred.reject({
+          target: this._request,
+          message: message
+        });
+        delete this._deferred;
+        return;
+      }
+
+      const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+      let gmpResults = [];
+      for (let i = 0; i < updatesElement.childNodes.length; ++i) {
+        let updatesChildElement = updatesElement.childNodes.item(i);
+        if (updatesChildElement.nodeType != ELEMENT_NODE) {
+          continue;
+        }
+        if (updatesChildElement.localName == "addons") {
+          gmpResults = GMPAddon.parseGMPAddonsNode(updatesChildElement);
+        }
+      }
+       this._deferred.resolve(gmpResults);
+       delete this._deferred;
+    } catch (e) {
+      this._deferred.reject({
+        target: this._request,
+        message: e
+      });
+      delete this._deferred;
+    }
+  },
+};
+
+/**
+ * Used to construct a single GMP addon
+ * GMPAddon objects are returns from GMPInstallManager.checkForAddons
+ * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
+ *
+ * @param gmpAddon The AUS response XML's DOM element `addon`
+ */
+function GMPAddon(gmpAddon) {
+  let log = getScopedLogger("GMPAddon.constructor");
+  gmpAddon.QueryInterface(Ci.nsIDOMElement);
+  ["id", "URL", "hashFunction",
+   "hashValue", "version", "size"].forEach(name => {
+    if (gmpAddon.hasAttribute(name)) {
+      this[name] = gmpAddon.getAttribute(name);
+    }
+  });
+  this.size = Number(this.size) || undefined;
+  log.info ("Created new addon: " + this.toString());
+}
+/**
+ * Parses an XML GMP addons node from AUS into an array
+ * @param addonsElement An nsIDOMElement compatible node with XML from AUS
+ * @return An array of GMPAddon results
+ */
+GMPAddon.parseGMPAddonsNode = function(addonsElement) {
+  let log = getScopedLogger("GMPAddon.parseGMPAddonsNode");
+  let gmpResults = [];
+  if (addonsElement.localName !== "addons") {
+    return;
+  }
+
+  addonsElement.QueryInterface(Ci.nsIDOMElement);
+  let addonCount = addonsElement.childNodes.length;
+  for (let i = 0; i < addonCount; ++i) {
+    let addonElement = addonsElement.childNodes.item(i);
+    if (addonElement.localName !== "addon") {
+      continue;
+    }
+    addonElement.QueryInterface(Ci.nsIDOMElement);
+    try {
+      gmpResults.push(new GMPAddon(addonElement));
+    } catch (e) {
+      log.warn("invalid addon: " + e);
+      continue;
+    }
+  }
+  return gmpResults;
+};
+GMPAddon.prototype = {
+  /**
+   * Returns a string representation of the addon
+   */
+  toString: function() {
+    return this.id + " (" +
+           "isValid: " + this.isValid +
+           ", isInstalled: " + this.isInstalled +
+           ", isOpenH264: " + this.isOpenH264 +
+           ", hashFunction: " + this.hashFunction+
+           ", hashValue: " + this.hashValue +
+           (this.size !== undefined ? ", size: " + this.size : "" ) +
+           ")";
+  },
+  /**
+   * If all the fields aren't specified don't consider this addon valid
+   * @return true if the addon is parsed and valid
+   */
+  get isValid() {
+    return this.id && this.URL && this.version &&
+      this.hashFunction && !!this.hashValue;
+  },
+  /**
+   * Open H264 has special handling.
+   * @return true if the plugin is the openh264 plugin
+   */
+  get isOpenH264() {
+    return this.id === OPEN_H264_ID;
+  },
+  get isInstalled() {
+    return this.version &&
+      GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, this.id) === this.version;
+  }
+};
+/**
+ * Constructs a GMPExtractor object which is used to extract a GMP zip
+ * into the specified location. (Which typically leties per platform)
+ * @param zipPath The path on disk of the zip file to extract
+ */
+function GMPExtractor(zipPath, installToDirPath) {
+    this.zipPath = zipPath;
+    this.installToDirPath = installToDirPath;
+}
+GMPExtractor.prototype = {
+  /**
+   * Obtains a list of all the entries in a zipfile in the format of *.*.
+   * This also includes files inside directories.
+   *
+   * @param zipReader the nsIZipReader to check
+   * @return An array of string name entries which can be used
+   *         in nsIZipReader.extract
+   */
+  _getZipEntries: function(zipReader) {
+    let entries = [];
+    let enumerator = zipReader.findEntries("*.*");
+    while (enumerator.hasMore()) {
+      entries.push(enumerator.getNext());
+    }
+    return entries;
+  },
+  /**
+   * Installs the this.zipPath contents into the directory used to store GMP
+   * addons for the current platform.
+   *
+   * @return a promise which will be resolved or rejected
+   *         See GMPInstallManager.installAddon for resolve/rejected info
+   */
+  install: function() {
+    try {
+      let log = getScopedLogger("GMPExtractor.install");
+      this._deferred = Promise.defer();
+      log.info("Installing " + this.zipPath + "...");
+      // Get the input zip file
+      let zipFile = Cc["@mozilla.org/file/local;1"].
+                    createInstance(Ci.nsIFile);
+      zipFile.initWithPath(this.zipPath);
+
+      // Initialize a zipReader and obtain the entries
+      var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+                      createInstance(Ci.nsIZipReader);
+      zipReader.open(zipFile)
+      let entries = this._getZipEntries(zipReader);
+      let extractedPaths = [];
+
+      // Extract each of the entries
+      entries.forEach(entry => {
+        // We don't need these types of files
+        if (entry.contains("__MACOSX")) {
+          return;
+        }
+        let outFile = Cc["@mozilla.org/file/local;1"].
+                      createInstance(Ci.nsILocalFile);
+        outFile.initWithPath(this.installToDirPath);
+        outFile.appendRelativePath(entry);
+
+        // Make sure the directory hierarchy exists
+        if(!outFile.parent.exists()) {
+          outFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+        }
+        zipReader.extract(entry, outFile);
+        extractedPaths.push(outFile.path);
+        log.info(entry + " was successfully extracted to: " +
+            outFile.path);
+      });
+      zipReader.close();
+      if (!GMPInstallManager.overrideLeaveDownloadedZip) {
+        zipFile.remove(false);
+      }
+
+      log.info(this.zipPath + " was installed successfully");
+      this._deferred.resolve(extractedPaths);
+    } catch (e) {
+      if (zipReader) {
+        zipReader.close();
+      }
+      this._deferred.reject({
+        target: this,
+        status: e,
+        type: "exception"
+      });
+    }
+    return this._deferred.promise;
+  }
+};
+
+
+/**
+ * Constructs an object which downloads and initiates an install of
+ * the specified GMPAddon object.
+ * @param gmpAddon The addon to install.
+ */
+function GMPDownloader(gmpAddon)
+{
+  this._gmpAddon = gmpAddon;
+}
+/**
+ * Computes the file hash of fileToHash with the specified hash function
+ * @param hashFunctionName A hash function name such as sha512
+ * @param fileToHash An nsIFile to hash
+ * @return a promise which resolve to a digest in binary hex format
+ */
+GMPDownloader.computeHash = function(hashFunctionName, fileToHash) {
+  let log = getScopedLogger("GMPDownloader.computeHash");
+  let digest;
+  let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+                   createInstance(Ci.nsIFileInputStream);
+  fileStream.init(fileToHash, FileUtils.MODE_RDONLY,
+                  FileUtils.PERMS_FILE, 0);
+  try {
+    let hash = Cc["@mozilla.org/security/hash;1"].
+               createInstance(Ci.nsICryptoHash);
+    let hashFunction =
+      Ci.nsICryptoHash[hashFunctionName.toUpperCase()];
+    if (!hashFunction) {
+      log.error("could not get hash function");
+      return Promise.reject();
+    }
+    hash.init(hashFunction);
+    hash.updateFromStream(fileStream, -1);
+    digest = binaryToHex(hash.finish(false));
+  } catch (e) {
+    log.warn("failed to compute hash: " + e);
+    digest = "";
+  }
+  fileStream.close();
+  return Promise.resolve(digest);
+},
+GMPDownloader.prototype = {
+  /**
+   * Starts the download process for an addon.
+   * @return a promise which will be resolved or rejected
+   *         See GMPInstallManager.installAddon for resolve/rejected info
+   */
+  start: function() {
+    let log = getScopedLogger("GMPDownloader.start");
+    this._deferred = Promise.defer();
+    if (!this._gmpAddon.isValid) {
+      log.info("gmpAddon is not valid, will not continue");
+      return Promise.reject({
+        target: this,
+        status: status,
+        type: "downloaderr"
+      });
+    }
+
+    let uri = Services.io.newURI(this._gmpAddon.URL, null, null);
+    this._request = Cc["@mozilla.org/network/incremental-download;1"].
+                    createInstance(Ci.nsIIncrementalDownload);
+    let gmpFile = FileUtils.getFile("TmpD", [this._gmpAddon.id + ".zip"]);
+    if (gmpFile.exists()) {
+      gmpFile.remove(false);
+    }
+
+    log.info("downloading from " + uri.spec + " to " + gmpFile.path);
+    this._request.init(uri, gmpFile, DOWNLOAD_CHUNK_BYTES_SIZE,
+                       DOWNLOAD_INTERVAL);
+    this._request.start(this, null);
+    return this._deferred.promise;
+  },
+  // For nsIRequestObserver
+  onStartRequest: function(request, context) {
+  },
+  // For nsIRequestObserver
+  // Called when the GMP addon zip file is downloaded
+  onStopRequest: function(request, context, status) {
+    let log = getScopedLogger("GMPDownloader.onStopRequest");
+    log.info("onStopRequest called");
+    if (!Components.isSuccessCode(status)) {
+      log.info("status failed: " + status);
+      this._deferred.reject({
+        target: this,
+        status: status,
+        type: "downloaderr"
+      });
+      return;
+    }
+
+    let promise = this._verifyDownload();
+    promise.then(() => {
+      log.info("GMP file is ready to unzip");
+      let destination = this._request.destination;
+
+      let zipPath = destination.path;
+      let gmpAddon = this._gmpAddon;
+      let installToDirPath = Cc["@mozilla.org/file/local;1"].
+                          createInstance(Ci.nsIFile);
+      let path = OS.Path.join(OS.Constants.Path.profileDir, gmpAddon.id);
+      installToDirPath.initWithPath(path);
+      log.info("install to directory path: " + installToDirPath.path);
+      let gmpInstaller = new GMPExtractor(zipPath, installToDirPath.path);
+      let installPromise = gmpInstaller.install();
+      installPromise.then(extractedPaths => {
+        // Success, set the prefs
+        let now = Math.round(Date.now() / 1000);
+        GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, now, gmpAddon.id);
+        GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH,
+                     installToDirPath.path, gmpAddon.id);
+        GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, gmpAddon.version,
+                     gmpAddon.id);
+        this._deferred.resolve(extractedPaths);
+      }, err => {
+        this._deferred.reject(err);
+      });
+    }, err => {
+      log.warn("verifyDownload check failed");
+      this._deferred.reject({
+        target: this,
+        status: 200,
+        type: "verifyerr"
+      });
+    });
+  },
+  /**
+   * Verifies that the downloaded zip file's hash matches the GMPAddon hash.
+   * @return a promise which resolves if the download verifies
+   */
+  _verifyDownload: function() {
+    let verifyDownloadDeferred = Promise.defer();
+    let log = getScopedLogger("GMPDownloader._verifyDownload");
+    log.info("_verifyDownload called");
+    if (!this._request) {
+      return Promise.reject();
+    }
+
+    let destination = this._request.destination;
+    log.info("for path: " + destination.path);
+
+    // Ensure that the file size matches the expected file size.
+    if (this._gmpAddon.size !== undefined &&
+        destination.fileSize != this._gmpAddon.size) {
+      log.warn("Downloader:_verifyDownload downloaded size " +
+               destination.fileSize + " != expected size " +
+               this._gmpAddon.size + ".");
+      return Promise.reject();
+    }
+
+    let promise = GMPDownloader.computeHash(this._gmpAddon.hashFunction, destination);
+    promise.then(digest => {
+        let expectedDigest = this._gmpAddon.hashValue.toLowerCase();
+        if (digest !== expectedDigest) {
+          log.warn("hashes do not match! Got: `" +
+                   digest + "`, expected: `" + expectedDigest +  "`");
+          this._deferred.reject();
+          return;
+        }
+
+        log.info("hashes match!");
+        verifyDownloadDeferred.resolve();
+    }, err => {
+        verifyDownloadDeferred.reject();
+    });
+    return verifyDownloadDeferred.promise;
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver])
+};
+
+/**
+ * Convert a string containing binary values to hex.
+ */
+function binaryToHex(input) {
+  let result = "";
+  for (let i = 0; i < input.length; ++i) {
+    let hex = input.charCodeAt(i).toString(16);
+    if (hex.length == 1)
+      hex = "0" + hex;
+    result += hex;
+  }
+  return result;
+}
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -55,16 +55,17 @@ EXTRA_JS_MODULES += [
     'Task.jsm',
     'TelemetryTimestamps.jsm',
     'Timer.jsm',
     'ZipUtils.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'CertUtils.jsm',
+    'GMPInstallManager.jsm',
     'ResetProfile.jsm',
     'Services.jsm',
     'Troubleshoot.jsm',
     'UpdateChannel.jsm',
     'WindowDraggingUtils.jsm',
     'WindowsPrefSync.jsm',
 ]
 
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
@@ -0,0 +1,725 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components;
+const URL_HOST = "http://localhost";
+
+Cu.import("resource://gre/modules/GMPInstallManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm")
+
+do_get_profile();
+
+function run_test() {Cu.import("resource://gre/modules/Preferences.jsm")
+  Preferences.set("media.gmp-manager.log", true);
+  run_next_test();
+}
+
+/**
+ * Tests that the helper used for preferences works correctly
+ */
+add_test(function test_prefs() {
+  let addon1 = "addon1", addon2 = "addon2";
+
+  GMPPrefs.set(GMPPrefs.KEY_LOG_ENABLED, true);
+  GMPPrefs.set(GMPPrefs.KEY_URL, "http://not-really-used");
+  GMPPrefs.set(GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2");
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "1", addon1);
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH, "2", addon1);
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "3", addon1);
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_LAST_UPDATE, "4", addon2);
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_PATH, "5", addon2);
+  GMPPrefs.set(GMPPrefs.KEY_ADDON_VERSION, "6", addon2);
+  GMPPrefs.set(GMPPrefs.KEY_CERT_CHECKATTRS, true);
+
+  do_check_true(GMPPrefs.get(GMPPrefs.KEY_LOG_ENABLED));
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL), "http://not-really-used");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_URL_OVERRIDE), "http://not-really-used-2");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, addon1), "1");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, addon1), "2");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, addon1), "3");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE, addon2), "4");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, addon2), "5");
+  do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, addon2), "6");
+  do_check_true(GMPPrefs.get(GMPPrefs.KEY_CERT_CHECKATTRS));
+  run_next_test();
+});
+
+/**
+ * Tests that an uninit without a check works fine
+ */
+add_test(function test_checkForAddons_noResponse() {
+  let installManager = new GMPInstallManager();
+  installManager.uninit();
+  run_next_test();
+});
+
+/**
+ * Tests that an uninit without an install works fine
+ */
+add_test(function test_checkForAddons_noResponse() {
+  overrideXHR(200, "");
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_throw("no repsonse should reject");
+  }, function(err) {
+      installManager.uninit();
+  });
+  run_next_test();
+});
+
+/**
+ * Tests that no response returned rejects
+ */
+add_test(function test_checkForAddons_noResponse() {
+  overrideXHR(200, "");
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_throw("no repsonse should reject");
+  }, function(err) {
+    do_check_true(!!err);
+    installManager.uninit();
+    run_next_test();
+  });
+});
+
+/**
+ * Tests that no addons element returned resolves with no addons
+ */
+add_task(function test_checkForAddons_noAddonsElement() {
+  overrideXHR(200, "<updates></updates>");
+  let installManager = new GMPInstallManager();
+  let gmpAddons = yield installManager.checkForAddons();
+  do_check_eq(gmpAddons.length, 0);
+  installManager.uninit();
+});
+
+/**
+ * Tests that empty addons element returned resolves with no addons
+ */
+add_task(function test_checkForAddons_noAddonsElement() {
+  overrideXHR(200, "<updates><addons/></updates>");
+  let installManager = new GMPInstallManager();
+  let gmpAddons = yield installManager.checkForAddons();
+  do_check_eq(gmpAddons.length, 0);
+  installManager.uninit();
+});
+
+/**
+ * Tests that a response with the wrong root element rejects
+ */
+add_test(function test_checkForAddons_wrongResponseXML() {
+  overrideXHR(200, "<digits_of_pi>3.141592653589793....</digits_of_pi>");
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(err, gmpAddons) {
+    do_throw("response with the wrong root element should reject");
+  }, function(err) {
+    do_check_true(!!err);
+    installManager.uninit();
+    run_next_test();
+  });
+});
+
+
+/**
+ * Tests that a 404 error works as expected
+ */
+add_test(function test_checkForAddons_404Error() {
+  overrideXHR(404, "");
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_throw("404 response should reject");
+  }, function(err) {
+    do_check_true(!!err);
+    do_check_eq(err.status, 404);
+    installManager.uninit();
+    run_next_test();
+  });
+});
+
+
+/**
+ * Tests that gettinga a funky non XML response works as expected
+ */
+add_test(function test_checkForAddons_notXML() {
+  overrideXHR(200, "3.141592653589793....");
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_throw("non XML response should reject");
+  }, function(err) {
+    do_check_true(!!err);
+    installManager.uninit();
+    run_next_test();
+  });
+});
+
+/**
+ * Tests that getting a response with a single addon works as expected
+ */
+add_test(function test_checkForAddons_singleAddonNoUpdates() {
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "<updates>" +
+    "    <addons>" +
+    "        <addon id=\"openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha256\"" +
+    "               hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"1.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 1);
+    let gmpAddon= gmpAddons[0];
+    do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
+    do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+    do_check_eq(gmpAddon.hashFunction, "sha256");
+    do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+    do_check_eq(gmpAddon.version, "1.1");
+    do_check_eq(gmpAddon.size, undefined);
+    do_check_true(gmpAddon.isValid);
+    do_check_true(gmpAddon.isOpenH264);
+    do_check_false(gmpAddon.isInstalled);
+    installManager.uninit();
+    run_next_test();
+  }, function(err) {
+    do_throw("1 addon found should not reject");
+  });
+});
+
+/**
+ * Tests that getting a response with a single addon with the optional size
+ * attribute parses as expected.
+ */
+add_test(function test_checkForAddons_singleAddonNoUpdates() {
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "<updates>" +
+    "    <addons>" +
+    "        <addon id=\"openh264-plugin-no-at-symbol\"" +
+    "               URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha256\"" +
+    "               size=\"42\"" +
+    "               hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"1.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 1);
+    let gmpAddon= gmpAddons[0];
+    do_check_eq(gmpAddon.id, "openh264-plugin-no-at-symbol");
+    do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+    do_check_eq(gmpAddon.hashFunction, "sha256");
+    do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+    do_check_eq(gmpAddon.size, 42);
+    do_check_eq(gmpAddon.version, "1.1");
+    do_check_true(gmpAddon.isValid);
+    do_check_false(gmpAddon.isOpenH264);
+    do_check_false(gmpAddon.isInstalled);
+    installManager.uninit();
+    run_next_test();
+  }, function(err) {
+    do_throw("1 addon found should not reject");
+  });
+});
+
+/**
+ * Tests that checking for multiple addons work correctly.
+ * Also tests that invalid addons work correctly.
+ */
+add_test(function test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() {
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "<updates>" +
+    "    <addons>" +
+    // valid openh264
+    "        <addon id=\"openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha256\"" +
+    "               hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"1.1\"/>" +
+    // valid not openh264
+    "        <addon id=\"NOT-openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha512\"" +
+    "               hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"9.1\"/>" +
+    // noid
+    "        <addon notid=\"NOT-openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha512\"" +
+    "               hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"9.1\"/>" +
+    // no URL
+    "        <addon id=\"NOT-openh264-plugin@cisco.com\"" +
+    "               notURL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha512\"" +
+    "               hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"9.1\"/>" +
+    // no hash function
+    "        <addon id=\"NOT-openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               nothashFunction=\"sha512\"" +
+    "               hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"9.1\"/>" +
+    // no hash function
+    "        <addon id=\"NOT-openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha512\"" +
+    "               nothashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"9.1\"/>" +
+    // not version
+    "        <addon id=\"NOT-openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha512\"" +
+    "               hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               notversion=\"9.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 7);
+    let gmpAddon= gmpAddons[0];
+    do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
+    do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+    do_check_eq(gmpAddon.hashFunction, "sha256");
+    do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+    do_check_eq(gmpAddon.version, "1.1");
+    do_check_true(gmpAddon.isValid);
+    do_check_true(gmpAddon.isOpenH264);
+    do_check_false(gmpAddon.isInstalled);
+
+    gmpAddon= gmpAddons[1];
+    do_check_eq(gmpAddon.id, "NOT-openh264-plugin@cisco.com");
+    do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip");
+    do_check_eq(gmpAddon.hashFunction, "sha512");
+    do_check_eq(gmpAddon.hashValue, "141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+    do_check_eq(gmpAddon.version, "9.1");
+    do_check_true(gmpAddon.isValid);
+    do_check_false(gmpAddon.isOpenH264);
+    do_check_false(gmpAddon.isInstalled);
+
+    for (let i = 2; i < gmpAddons.length; i++) {
+      do_check_false(gmpAddons[i].isValid);
+      do_check_false(gmpAddons[i].isInstalled);
+    }
+    installManager.uninit();
+    run_next_test();
+  }, function(err) {
+    do_throw("multiple addons found should not reject");
+  });
+});
+
+/**
+ * Tests that checking for addons when there are also updates available
+ * works as expected.
+ */
+add_test(function test_checkForAddons_updatesWithAddons() {
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "    <updates>" +
+    "        <update type=\"minor\" displayVersion=\"33.0a1\" appVersion=\"33.0a1\" platformVersion=\"33.0a1\" buildID=\"20140628030201\">" +
+    "        <patch type=\"complete\" URL=\"http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/2014/06/2014-06-28-03-02-01-mozilla-central/firefox-33.0a1.en-US.mac.complete.mar\" hashFunction=\"sha512\" hashValue=\"f3f90d71dff03ae81def80e64bba3e4569da99c9e15269f731c2b167c4fc30b3aed9f5fee81c19614120230ca333e73a5e7def1b8e45d03135b2069c26736219\" size=\"85249896\"/>" +
+    "    </update>" +
+    "    <addons>" +
+    "        <addon id=\"openh264-plugin@cisco.com\"" +
+    "               URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+    "               hashFunction=\"sha256\"" +
+    "               hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+    "               version=\"1.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let promise = installManager.checkForAddons();
+  promise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 1);
+    let gmpAddon= gmpAddons[0];
+    do_check_eq(gmpAddon.id, "openh264-plugin@cisco.com");
+    do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+    do_check_eq(gmpAddon.hashFunction, "sha256");
+    do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+    do_check_eq(gmpAddon.version, "1.1");
+    do_check_true(gmpAddon.isValid);
+    do_check_true(gmpAddon.isOpenH264);
+    do_check_false(gmpAddon.isInstalled);
+    installManager.uninit();
+    run_next_test();
+  }, function(err) {
+    do_throw("updates with addons should not reject");
+  });
+});
+
+/**
+ * Tests that installing found addons works as expected
+ */
+function test_checkForAddons_installAddon(id, includeSize,wantInstallReject) {
+  do_print("Running installAddon for includeSize: " + includeSize +
+           " and wantInstallReject: " + wantInstallReject);
+  let httpServer = new HttpServer();
+  let dir = FileUtils.getDir("TmpD", [], true);
+  httpServer.registerDirectory("/", dir);
+  httpServer.start(-1);
+  let testserverPort = httpServer.identity.primaryPort;
+  let zipFileName = "test_" + id + "_GMP.zip";
+
+  let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName;
+  do_print("zipURL: " + zipURL);
+
+  let data = "e~=0.5772156649";
+  let zipFile = createNewZipFile(zipFileName, data);
+  let hashFunc = "sha256";
+  let expectedDigest = yield GMPDownloader.computeHash(hashFunc, zipFile);
+
+  let fileSize = zipFile.size;
+  if (wantInstallReject) {
+      fileSize = 1;
+  }
+
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "<updates>" +
+    "    <addons>" +
+    "        <addon id=\"" + id + "-openh264-plugin@cisco.com\"" +
+    "               URL=\"" + zipURL + "\"" +
+    "               hashFunction=\"" + hashFunc + "\"" +
+    "               hashValue=\"" + expectedDigest + "\"" +
+    (includeSize ? " size=\"" + fileSize + "\"" : "") +
+    "               version=\"1.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let checkPromise = installManager.checkForAddons();
+  checkPromise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 1);
+    let gmpAddon = gmpAddons[0];
+    do_check_false(gmpAddon.isInstalled);
+
+    GMPInstallManager.overrideLeaveDownloadedZip = true;
+    let installPromise = installManager.installAddon(gmpAddon);
+    installPromise.then(function(extractedPaths) {
+      if (wantInstallReject) {
+        do_throw("install update should reject");
+      }
+      do_check_eq(extractedPaths.length, 1);
+      let extractedPath = extractedPaths[0];
+
+      do_print("Extracted path: " + extractedPath);
+
+      let extractedFile = Cc["@mozilla.org/file/local;1"].
+                          createInstance(Ci.nsIFile);
+      extractedFile.initWithPath(extractedPath);
+      do_check_true(extractedFile.exists());
+      let readData = readStringFromFile(extractedFile);
+      do_check_eq(readData, data);
+
+      // Check that the downloaded zip mathces the offered zip exactly
+      let downloadedGMPFile = FileUtils.getFile("TmpD",
+        [gmpAddon.id + ".zip"]);
+      do_check_true(downloadedGMPFile.exists());
+      let downloadedBytes = getBinaryFileData(downloadedGMPFile);
+      let sourceBytes = getBinaryFileData(zipFile);
+      do_check_true(compareBinaryData(downloadedBytes, sourceBytes));
+
+      // Make sure the prefs are set correctly
+      do_check_true(!!GMPPrefs.get(GMPPrefs.KEY_ADDON_LAST_UPDATE,
+                                   gmpAddon.id, ""));
+      do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_PATH, gmpAddon.id, ""),
+                               extractedFile.parent.path);
+      do_check_eq(GMPPrefs.get(GMPPrefs.KEY_ADDON_VERSION, gmpAddon.id, ""),
+                               "1.1");
+      // Make sure it reports as being installed
+      do_check_true(gmpAddon.isInstalled);
+
+      // Cleanup
+      extractedFile.parent.remove(true);
+      zipFile.remove(false);
+      httpServer.stop(function() {});
+      do_print("Removing downloaded GMP file: " + downloadedGMPFile.path);
+      downloadedGMPFile.remove(false);
+      installManager.uninit();
+    }, function(err) {
+      zipFile.remove(false);
+      let downloadedGMPFile = FileUtils.getFile("TmpD",
+        [gmpAddon.id + ".zip"]);
+      do_print("Removing from err downloaded GMP file: " +
+               downloadedGMPFile.path);
+      downloadedGMPFile.remove(false);
+      if (!wantInstallReject) {
+        do_throw("install update should not reject");
+      }
+    });
+  }, function(err) {
+    do_throw("checking updates to install them should not reject");
+  });
+}
+
+add_task(test_checkForAddons_installAddon.bind(null, "1", true, false));
+add_task(test_checkForAddons_installAddon.bind(null, "2", false, false));
+add_task(test_checkForAddons_installAddon.bind(null, "3", true, true));
+
+/**
+ * Tests that installing addons when there is no server works as expected
+ */
+add_test(function test_installAddon_noServer() {
+  let dir = FileUtils.getDir("TmpD", [], true);
+  let zipFileName = "test_GMP.zip";
+  let zipURL = URL_HOST + ":0/" + zipFileName;
+
+  let data = "e~=0.5772156649";
+  let zipFile = createNewZipFile(zipFileName, data);
+
+  let responseXML =
+    "<?xml version=\"1.0\"?>" +
+    "<updates>" +
+    "    <addons>" +
+    "        <addon id=\"openh264-plugin@cisco.com\"" +
+    "               URL=\"" + zipURL + "\"" +
+    "               hashFunction=\"sha256\"" +
+    "               hashValue=\"11221cbda000347b054028b527a60e578f919cb10f322ef8077d3491c6fcb474\"" +
+    "               version=\"1.1\"/>" +
+    "  </addons>" +
+    "</updates>"
+
+  overrideXHR(200, responseXML);
+  let installManager = new GMPInstallManager();
+  let checkPromise = installManager.checkForAddons();
+  checkPromise.then(function(gmpAddons) {
+    do_check_eq(gmpAddons.length, 1);
+    let gmpAddon= gmpAddons[0];
+
+    GMPInstallManager.overrideLeaveDownloadedZip = true;
+    let installPromise = installManager.installAddon(gmpAddon);
+    installPromise.then(function(extractedPaths) {
+        do_throw("No server for install should reject");
+    }, function(err) {
+      do_check_true(!!err);
+      installManager.uninit();
+      run_next_test();
+    });
+  }, function(err) {
+    do_throw("check should not reject for installn o server");
+  });
+});
+
+/**
+ * Returns the read stream into a string
+ */
+function readStringFromInputStream(inputStream) {
+  let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+            createInstance(Ci.nsIScriptableInputStream);
+  sis.init(inputStream);
+  let text = sis.read(sis.available());
+  sis.close();
+  return text;
+}
+
+/**
+ * Reads a string of text from a file.
+ * This function only works with ASCII text.
+ */
+function readStringFromFile(file) {
+  if (!file.exists()) {
+    do_print("readStringFromFile - file doesn't exist: " + file.path);
+    return null;
+  }
+  let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+            createInstance(Ci.nsIFileInputStream);
+  fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+  return readStringFromInputStream(fis);
+}
+
+/**
+ * Bare bones XMLHttpRequest implementation for testing onprogress, onerror,
+ * and onload nsIDomEventListener handleEvent.
+ */
+function makeHandler(aVal) {
+  if (typeof aVal == "function")
+    return { handleEvent: aVal };
+  return aVal;
+}
+/**
+ * Constructs a mock xhr which is used for testing different aspects
+ * of responses.
+ */
+function xhr(inputStatus, inputResponse) {
+  this.inputStatus = inputStatus;
+  this.inputResponse = inputResponse;
+}
+xhr.prototype = {
+  overrideMimeType: function(aMimetype) { },
+  setRequestHeader: function(aHeader, aValue) { },
+  status: null,
+  channel: { set notificationCallbacks(aVal) { } },
+  _url: null,
+  _method: null,
+  open: function(aMethod, aUrl) {
+    this.channel.originalURI = Services.io.newURI(aUrl, null, null);
+    this._method = aMethod; this._url = aUrl;
+  },
+  abort: function() {
+  },
+  responseXML: null,
+  responseText: null,
+  send: function(aBody) {
+    let self = this;
+    do_execute_soon(function() {
+      self.status = self.inputStatus;
+      self.responseText = self.inputResponse;
+      try {
+        let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+                     createInstance(Ci.nsIDOMParser);
+        self.responseXML = parser.parseFromString(self.inputResponse,
+                                                  "application/xml");
+      } catch (e) {
+        self.responseXML = null;
+      }
+      let e = { target: self };
+      if (self.inputStatus === 200) {
+        self.onload(e);
+      } else {
+        self.onerror(e);
+      }
+    });
+  },
+  _onprogress: null,
+  set onprogress(aValue) { this._onprogress = makeHandler(aValue); },
+  get onprogress() { return this._onprogress; },
+  _onerror: null,
+  set onerror(aValue) { this._onerror = makeHandler(aValue); },
+  get onerror() { return this._onerror; },
+  _onload: null,
+  set onload(aValue) { this._onload = makeHandler(aValue); },
+  get onload() { return this._onload; },
+  addEventListener: function(aEvent, aValue, aCapturing) {
+    eval("this._on" + aEvent + " = aValue");
+  },
+  flags: Ci.nsIClassInfo.SINGLETON,
+  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
+  getHelperForLanguage: function(aLanguage) null,
+  getInterfaces: function(aCount) {
+    let interfaces = [Ci.nsISupports];
+    aCount.value = interfaces.length;
+    return interfaces;
+  },
+  classDescription: "XMLHttpRequest",
+  contractID: "@mozilla.org/xmlextras/xmlhttprequest;1",
+  classID: Components.ID("{c9b37f43-4278-4304-a5e0-600991ab08cb}"),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter == null)
+      return this.QueryInterface(aIID);
+    throw Cr.NS_ERROR_NO_AGGREGATION;
+  },
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Ci.nsIClassInfo) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+  get wrappedJSObject() { return this; }
+};
+
+/**
+ * Helper used to overrideXHR requests (no matter to what URL) with the
+ * specified status and response.
+ * @param status The status you want to get back when an XHR request is made
+ * @param response The response you want to get back when an XHR request is made
+ */
+function overrideXHR(status, response) {
+  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  if (overrideXHR.myxhr) {
+    registrar.unregisterFactory(overrideXHR.myxhr.classID, overrideXHR.myxhr);
+  }
+  overrideXHR.myxhr = new xhr(status, response);
+  registrar.registerFactory(overrideXHR.myxhr.classID,
+                            overrideXHR.myxhr.classDescription,
+                            overrideXHR.myxhr.contractID,
+                            overrideXHR.myxhr);
+}
+
+/**
+ * Compares binary data of 2 arrays and returns true if they are the same
+ *
+ * @param arr1 The first array to compare
+ * @param arr2 The second array to compare
+*/
+function compareBinaryData(arr1, arr2) {
+  do_check_eq(arr1.length, arr2.length);
+  for (let i = 0; i < arr1.length; i++) {
+    if (arr1[i] != arr2[i]) {
+      do_print("Data differs at index " + i +
+               ", arr1: " + arr1[i] + ", arr2: " + arr2[i]);
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * Reads a file's data and returns it
+ *
+ * @param file The file to read the data from
+ * @return array of bytes for the data in the file.
+*/
+function getBinaryFileData(file) {
+  let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+                   createInstance(Ci.nsIFileInputStream);
+  // Open as RD_ONLY with default permissions.
+  fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+
+  // Check the returned size versus the expected size.
+  let stream = Cc["@mozilla.org/binaryinputstream;1"].
+               createInstance(Ci.nsIBinaryInputStream);
+  stream.setInputStream(fileStream);
+  let bytes = stream.readByteArray(stream.available());
+  fileStream.close();
+  return bytes;
+}
+
+/**
+ * Creates a new zip file containing a file with the specified data
+ * @param zipName The name of the zip file
+ * @param data The data to go inside the zip for the filename entry1.info
+ */
+function createNewZipFile(zipName, data) {
+   // Create a zip file which will be used for extracting
+    let stream = Cc["@mozilla.org/io/string-input-stream;1"].
+                 createInstance(Ci.nsIStringInputStream);
+    stream.setData(data, data.length);
+    let zipWriter = Cc["@mozilla.org/zipwriter;1"].
+                    createInstance(Components.interfaces.nsIZipWriter);
+    let zipFile = FileUtils.getFile("TmpD", [zipName]);
+    if (zipFile.exists()) {
+      zipFile.remove(false);
+    }
+    // From prio.h
+    const PR_RDWR = 0x04;
+    const PR_CREATE_FILE = 0x08;
+    const PR_TRUNCATE    = 0x20;
+    zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+    zipWriter.addEntryStream("entry1.info", Date.now(),
+                             Ci.nsIZipWriter.COMPRESSION_BEST, stream, false);
+    zipWriter.close();
+    stream.close();
+    do_print("zip file created on disk at: " + zipFile.path);
+    return zipFile;
+}
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -8,16 +8,17 @@ support-files =
   zips/zen.zip
 
 [test_AsyncShutdown.js]
 [test_BinarySearch.js]
 [test_DeferredTask.js]
 [test_dict.js]
 [test_DirectoryLinksProvider.js]
 [test_FileUtils.js]
+[test_GMPInstallManager.js]
 [test_Http.js]
 [test_Log.js]
 [test_NewTabUtils.js]
 [test_PermissionsUtils.js]
 [test_Preferences.js]
 [test_Promise.js]
 [test_propertyListsUtils.js]
 [test_readCertPrefs.js]