Bug 1009816 - Firefox desktop: openh264 updates: check, download, install. r=rstrong, gfritzsche a=kwierso
authorBrian R. Bondy <netzen@gmail.com>
Thu, 17 Jul 2014 21:46:10 -0400
changeset 216928 1f124b3a13555227443b93b695f189ff6e789bf0
parent 216927 96a0be5c760b3298a9da679aac970b8819cc22a5
child 216929 d774179cdc65b29b0e6b291accce71e3da4bcd83
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrstrong, gfritzsche, kwierso
bugs1009816
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1009816 - Firefox desktop: openh264 updates: check, download, install. r=rstrong, gfritzsche a=kwierso
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]