Bug 846019 - Part 1: amIAddonManager: Map URIs to AddonIDs. r=bmcbride
authorNils Maier <maierman@web.de>
Fri, 14 Jun 2013 22:48:06 -0400
changeset 135179 a331728c7d97a7382ed89c7b06329619e8ffe004
parent 135178 02fad60002a989d8f9e27fa316d43d9e8d031f21
child 135180 6503457c2561b84d5939ebdf81b574836f603a05
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersbmcbride
bugs846019
milestone24.0a1
Bug 846019 - Part 1: amIAddonManager: Map URIs to AddonIDs. r=bmcbride
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/amIAddonManager.idl
toolkit/mozapps/extensions/moz.build
toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js
toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1451,16 +1451,45 @@ var AddonManagerInternal = {
     if (!gStarted)
       throw Components.Exception("AddonManager is not initialized",
                                  Cr.NS_ERROR_NOT_INITIALIZED);
 
     this.getInstallsByTypes(null, aCallback);
   },
 
   /**
+   * Synchronously map a URI to the corresponding Addon ID.
+   *
+   * Mappable URIs are limited to in-application resources belonging to the
+   * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+   * but do not include URIs from meta data, such as the add-on homepage.
+   *
+   * @param  aURI
+   *         nsIURI to map to an addon id
+   * @return string containing the Addon ID or null
+   * @see    amIAddonManager.mapURIToAddonID
+   */
+  mapURIToAddonID: function AMI_mapURIToAddonID(aURI) {
+    if (!(aURI instanceof Ci.nsIURI)) {
+      throw Components.Exception("aURI is not a nsIURI",
+                                 Cr.NS_ERROR_INVALID_ARG);
+    }
+    // Try all providers
+    let providers = this.providers.slice(0);
+    for (let provider of providers) {
+      var id = callProvider(provider, "mapURIToAddonID", null, aURI);
+      if (id !== null) {
+        return id;
+      }
+    }
+
+    return null;
+  },
+
+  /**
    * Checks whether installation is enabled for a particular mimetype.
    *
    * @param  aMimetype
    *         The mimetype to check
    * @return true if installation is enabled for the mimetype
    */
   isInstallEnabled: function AMI_isInstallEnabled(aMimetype) {
     if (!gStarted)
@@ -2335,16 +2364,20 @@ this.AddonManager = {
   getInstallsByTypes: function AM_getInstallsByTypes(aTypes, aCallback) {
     AddonManagerInternal.getInstallsByTypes(aTypes, aCallback);
   },
 
   getAllInstalls: function AM_getAllInstalls(aCallback) {
     AddonManagerInternal.getAllInstalls(aCallback);
   },
 
+  mapURIToAddonID: function AM_mapURIToAddonID(aURI) {
+    return AddonManagerInternal.mapURIToAddonID(aURI);
+  },
+
   isInstallEnabled: function AM_isInstallEnabled(aType) {
     return AddonManagerInternal.isInstallEnabled(aType);
   },
 
   isInstallAllowed: function AM_isInstallAllowed(aType, aUri) {
     return AddonManagerInternal.isInstallAllowed(aType, aUri);
   },
 
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -20,16 +20,28 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "ChromeRegistry",
+                                   "@mozilla.org/chrome/chrome-registry;1",
+                                   "nsIChromeRegistry");
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "ResProtocolHandler",
+                                   "@mozilla.org/network/protocol;1?name=resource",
+                                   "nsIResProtocolHandler");
+
+const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+                                       "initWithPath");
+
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_INSTALL_CACHE              = "extensions.installCache";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 const PREF_EM_DSS_ENABLED             = "extensions.dss.enabled";
 const PREF_DSS_SWITCHPENDING          = "extensions.dss.switchPending";
@@ -1503,16 +1515,133 @@ var XPIProvider = {
   // A string listing the enabled add-ons for annotating crash reports
   enabledAddons: null,
   // An array of add-on IDs of add-ons that were inactive during startup
   inactiveAddonIDs: [],
   // Count of unpacked add-ons
   unpackedAddons: 0,
 
   /**
+   * Adds or updates a URI mapping for an Addon.id.
+   *
+   * Mappings should not be removed at any point. This is so that the mappings
+   * will be still valid after an add-on gets disabled or uninstalled, as
+   * consumers may still have URIs of (leaked) resources they want to map.
+   */
+  _addURIMapping: function XPI__addURIMapping(aID, aFile) {
+    try {
+      // Always use our own mechanics instead of nsIIOService.newFileURI, so
+      // that we can be sure to map things as we want them mapped.
+      let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, "."));
+      if (!uri) {
+        throw new Error("Cannot resolve");
+      }
+      this._ensureURIMappings();
+      this._uriMappings[aID] = uri.spec;
+    }
+    catch (ex) {
+      WARN("Failed to add URI mapping", ex);
+    }
+  },
+
+  /**
+   * Ensures that the URI to Addon mappings are available.
+   *
+   * The function will add mappings for all non-bootstrapped but enabled
+   * add-ons.
+   * Bootstrapped add-on mappings will be added directly when the bootstrap
+   * scope get loaded. (See XPIProvider._addURIMapping() and callers)
+   */
+  _ensureURIMappings: function XPI__ensureURIMappings() {
+    if (this._uriMappings) {
+      return;
+    }
+    // XXX Convert to Map(), once it gets stable with stable iterators
+    this._uriMappings = Object.create(null);
+
+    // XXX Convert to Set(), once it gets stable with stable iterators
+    let enabled = Object.create(null);
+    for (let a of this.enabledAddons.split(",")) {
+      a = decodeURIComponent(a.split(":")[0]);
+      enabled[a] = null;
+    }
+
+    let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"));
+    for (let loc of cache) {
+      for (let [id, val] in Iterator(loc.addons)) {
+        if (!(id in enabled)) {
+          continue;
+        }
+        let file = new nsIFile(val.descriptor);
+        let spec = Services.io.newFileURI(file).spec;
+        this._uriMappings[id] = spec;
+      }
+    }
+  },
+
+  /**
+   * Resolve a URI back to physical file.
+   *
+   * Of course, this works only for URIs pointing to local resources.
+   *
+   * @param  aURI
+   *         URI to resolve
+   * @return
+   *         resolved nsIFileURL
+   */
+  _resolveURIToFile: function XPI__resolveURIToFile(aURI) {
+    switch (aURI.scheme) {
+      case "jar":
+      case "file":
+        if (aURI instanceof Ci.nsIJARURI) {
+          return this._resolveURIToFile(aURI.JARFile);
+        }
+        return aURI;
+
+      case "chrome":
+        aURI = ChromeRegistry.convertChromeURL(aURI);
+        return this._resolveURIToFile(aURI);
+
+      case "resource":
+        aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
+                                  null);
+        return this._resolveURIToFile(aURI);
+
+      case "view-source":
+        aURI = Services.io.newURI(aURI.path, null, null);
+        return this._resolveURIToFile(aURI);
+
+      case "about":
+        if (aURI.spec == "about:blank") {
+          // Do not attempt to map about:blank
+          return null;
+        }
+
+        let chan;
+        try {
+          chan = Services.io.newChannelFromURI(aURI);
+        }
+        catch (ex) {
+          return null;
+        }
+        // Avoid looping
+        if (chan.URI.equals(aURI)) {
+          return null;
+        }
+        // We want to clone the channel URI to avoid accidentially keeping
+        // unnecessary references to the channel or implementation details
+        // around.
+        return this._resolveURIToFile(chan.URI.clone());
+
+      default:
+        return null;
+    }
+  },
+
+  /**
    * Starts the XPI provider initializes the install locations and prefs.
    *
    * @param  aAppChanged
    *         A tri-state value. Undefined means the current profile was created
    *         for this session, true means the profile already existed but was
    *         last used with an application with a different version number,
    *         false means that the profile was last used by this version of the
    *         application.
@@ -1727,16 +1856,19 @@ var XPIProvider = {
 
     this.installs = null;
     this.installLocations = null;
     this.installLocationsByName = null;
 
     // This is needed to allow xpcshell tests to simulate a restart
     this.extensionsActive = false;
 
+    // Remove URI mappings again
+    delete this._uriMappings;
+
     if (gLazyObjectsLoaded) {
       XPIDatabase.shutdown(function shutdownCallback() {
         Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
       });
     }
     else {
       Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
     }
@@ -3354,16 +3486,44 @@ var XPIProvider = {
     this.installs.forEach(function(aInstall) {
       if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
         results.push(aInstall.wrapper);
     });
     aCallback(results);
   },
 
   /**
+   * Synchronously map a URI to the corresponding Addon ID.
+   *
+   * Mappable URIs are limited to in-application resources belonging to the
+   * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+   * but do not include URIs from meta data, such as the add-on homepage.
+   *
+   * @param  aURI
+   *         nsIURI to map or null
+   * @return string containing the Addon ID
+   * @see    AddonManager.mapURIToAddonID
+   * @see    amIAddonManager.mapURIToAddonID
+   */
+  mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
+    this._ensureURIMappings();
+    let resolved = this._resolveURIToFile(aURI);
+    if (!resolved) {
+      return null;
+    }
+    resolved = resolved.spec;
+    for (let [id, spec] in Iterator(this._uriMappings)) {
+      if (resolved.startsWith(spec)) {
+        return id;
+      }
+    }
+    return null;
+  },
+
+  /**
    * Called when a new add-on has been enabled when only one add-on of that type
    * can be enabled.
    *
    * @param  aId
    *         The ID of the newly enabled add-on
    * @param  aType
    *         The type of the newly enabled add-on
    * @param  aPendingRestart
@@ -3717,16 +3877,19 @@ var XPIProvider = {
 
     let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
     this.bootstrapScopes[aId] = new Components.utils.Sandbox(principal,
                                                              {sandboxName: uri});
 
     let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
                  createInstance(Ci.mozIJSSubScriptLoader);
 
+    // Add a mapping for XPIProvider.mapURIToAddonID
+    this._addURIMapping(aId, aFile);
+
     try {
       // Copy the reason values from the global object into the bootstrap scope.
       for (let name in BOOTSTRAP_REASONS)
         this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
 
       // Add other stuff that extensions want.
       const features = [ "Worker", "ChromeWorker" ];
 
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -49,16 +49,24 @@ function amManager() {
 
 amManager.prototype = {
   observe: function AMC_observe(aSubject, aTopic, aData) {
     if (aTopic == "addons-startup")
       AddonManagerPrivate.startup();
   },
 
   /**
+   * @see amIAddonManager.idl
+   */
+  mapURIToAddonID: function AMC_mapURIToAddonID(uri, id) {
+    id.value = AddonManager.mapURIToAddonID(uri);
+    return !!id.value;
+  },
+
+  /**
    * @see amIWebInstaller.idl
    */
   isInstallEnabled: function AMC_isInstallEnabled(aMimetype, aReferer) {
     return AddonManager.isInstallEnabled(aMimetype);
   },
 
   /**
    * @see amIWebInstaller.idl
@@ -196,14 +204,15 @@ amManager.prototype = {
         throw Components.Exception("Component does not support aggregation",
                                    Cr.NS_ERROR_NO_AGGREGATION);
   
       if (!gSingleton)
         gSingleton = new amManager();
       return gSingleton.QueryInterface(aIid);
     }
   },
-  QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstaller,
+  QueryInterface: XPCOMUtils.generateQI([Ci.amIAddonManager,
+                                         Ci.amIWebInstaller,
                                          Ci.nsITimerCallback,
                                          Ci.nsIObserver])
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([amManager]);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIAddonManager.idl
@@ -0,0 +1,29 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * A service to make some AddonManager functionality available to C++ callers.
+ * Javascript callers should still use AddonManager.jsm directly.
+ */
+[scriptable, function, uuid(7b45d82d-7ad5-48d7-9b05-f32eb9818cd4)]
+interface amIAddonManager : nsISupports
+{
+  /**
+   * Synchronously map a URI to the corresponding Addon ID.
+   *
+   * Mappable URIs are limited to in-application resources belonging to the
+   * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
+   * but do not include URIs from meta data, such as the add-on homepage.
+   *
+   * @param  aURI
+   *         The nsIURI to map
+   * @return
+   *         true if the URI has been mapped successfully to an Addon ID
+   */
+  boolean mapURIToAddonID(in nsIURI aURI, out AUTF8String aID);
+};
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['test']
 
 XPIDL_SOURCES += [
+    'amIAddonManager.idl',
     'amIInstallTrigger.idl',
     'amIWebInstallListener.idl',
     'amIWebInstaller.idl',
 ]
 
 MODULE = 'extensions'
 
 EXTRA_COMPONENTS += [
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js
@@ -0,0 +1,263 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons URIs can be mapped to add-on IDs
+//
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// Enable loading extensions from the user scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+                          AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const userExtDir = gProfD.clone();
+userExtDir.append("extensions2");
+userExtDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userExtDir.parent);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver;
+
+function TestProvider(result) {
+  this.result = result;
+}
+TestProvider.prototype = {
+  uri: Services.io.newURI("hellow://world", null, null),
+  id: "valid@id",
+  startup: function() {},
+  shutdown: function() {},
+  mapURIToAddonID: function(aURI) {
+    if (aURI.spec === this.uri.spec) {
+      return this.id;
+    }
+    throw Components.Exception("Not mapped", result);
+  }
+};
+
+function TestProviderNoMap() {}
+TestProviderNoMap.prototype = {
+  startup: function() {},
+  shutdown: function() {}
+};
+
+function check_mapping(uri, id) {
+  do_check_eq(AddonManager.mapURIToAddonID(uri), id);
+  let svc = Components.classes["@mozilla.org/addons/integration;1"].
+            getService(Components.interfaces.amIAddonManager);
+  let val = {};
+  do_check_true(svc.mapURIToAddonID(uri, val));
+  do_check_eq(val.value, id);
+}
+
+function resetPrefs() {
+  Services.prefs.setIntPref("bootstraptest.active_version", -1);
+}
+
+function waitForPref(aPref, aCallback) {
+  function prefChanged() {
+    Services.prefs.removeObserver(aPref, prefChanged);
+    aCallback();
+  }
+  Services.prefs.addObserver(aPref, prefChanged, false);
+}
+
+function getActiveVersion() {
+  return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+  do_test_pending();
+
+  resetPrefs();
+
+  // Create and configure the HTTP server.
+  testserver = new HttpServer();
+  testserver.registerDirectory("/addons/", do_get_file("addons"));
+  testserver.start(4444);
+
+  startupManager();
+
+  run_test_nomapping();
+}
+
+function run_test_nomapping() {
+  do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
+  try {
+    let svc = Components.classes["@mozilla.org/addons/integration;1"].
+              getService(Components.interfaces.amIAddonManager);
+    let val = {};
+    do_check_false(svc.mapURIToAddonID(TestProvider.prototype.uri, val));
+  }
+  catch (ex) {
+    do_throw(ex);
+  }
+
+  run_test_1();
+}
+
+
+// Tests that add-on URIs are mappable after an install
+function run_test_1() {
+  prepare_test({ }, [
+    "onNewInstall"
+  ]);
+
+  AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), function(install) {
+    ensure_test_completed();
+
+    let addon = install.addon;
+    prepare_test({
+      "bootstrap1@tests.mozilla.org": [
+        ["onInstalling", false],
+        "onInstalled"
+      ]
+    }, [
+      "onInstallStarted",
+      "onInstallEnded",
+    ], function() {
+      let uri = addon.getResourceURI(".");
+      check_mapping(uri, addon.id);
+
+      waitForPref("bootstraptest.active_version", function() {
+        run_test_2(uri);
+      });
+    });
+    install.install();
+  });
+}
+
+// Tests that add-on URIs are still mappable, even after the add-on gets
+// disabled in-session.
+function run_test_2(uri) {
+  AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+    prepare_test({
+      "bootstrap1@tests.mozilla.org": [
+        ["onDisabling", false],
+        "onDisabled"
+      ]
+    });
+
+    b1.userDisabled = true;
+    ensure_test_completed();
+
+    AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
+      do_check_true(newb1.userDisabled);
+      check_mapping(uri, newb1.id);
+
+      run_test_3(uri);
+    });
+  });
+}
+
+// Tests that add-on URIs aren't mappable if the add-on was never started in a
+// session
+function run_test_3(uri) {
+  restartManager();
+
+  do_check_eq(AddonManager.mapURIToAddonID(uri), null);
+
+  run_test_4();
+}
+
+// Tests that add-on URIs are mappable after a restart + reenable
+function run_test_4() {
+  restartManager();
+
+  AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+    prepare_test({
+      "bootstrap1@tests.mozilla.org": [
+        ["onEnabling", false],
+        "onEnabled"
+      ]
+    });
+
+    b1.userDisabled = false;
+    ensure_test_completed();
+
+    AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
+      let uri = newb1.getResourceURI(".");
+      check_mapping(uri, newb1.id);
+
+      run_test_5();
+    });
+  });
+}
+
+// Tests that add-on URIs are mappable after a restart
+function run_test_5() {
+  restartManager();
+
+  AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+    let uri = b1.getResourceURI(".");
+    check_mapping(uri, b1.id);
+
+    run_test_invalidarg();
+  });
+}
+
+// Tests that the AddonManager will bail when mapURIToAddonID is called with an
+// invalid argument
+function run_test_invalidarg() {
+  restartManager();
+
+  let tests = [undefined,
+               null,
+               1,
+               "string",
+               "chrome://global/content/",
+               function() {}
+               ];
+  for (var test of tests) {
+    try {
+      AddonManager.mapURIToAddonID(test);
+      throw new Error("Shouldn't be able to map the URI in question");
+    }
+    catch (ex if ex.result) {
+      do_check_eq(ex.result, Components.results.NS_ERROR_INVALID_ARG);
+    }
+    catch (ex) {
+      do_throw(ex);
+    }
+  }
+
+  run_test_provider();
+}
+
+// Tests that custom providers are correctly handled
+function run_test_provider() {
+  restartManager();
+
+  const provider = new TestProvider(Components.results.NS_ERROR_NOT_AVAILABLE);
+  AddonManagerPrivate.registerProvider(provider);
+
+  check_mapping(provider.uri, provider.id);
+
+  let u2 = provider.uri.clone();
+  u2.path = "notmapped";
+  do_check_eq(AddonManager.mapURIToAddonID(u2), null);
+
+  AddonManagerPrivate.unregisterProvider(provider);
+
+  run_test_provider_nomap();
+}
+
+// Tests that custom providers are correctly handled, even not implementing
+// mapURIToAddonID
+function run_test_provider_nomap() {
+  restartManager();
+
+  const provider = new TestProviderNoMap();
+  AddonManagerPrivate.registerProvider(provider);
+
+  do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
+
+  AddonManagerPrivate.unregisterProvider(provider);
+
+  do_test_finished();
+}
+
+
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -3,17 +3,18 @@
  */
 
 // Verify that API functions fail if the Add-ons Manager isn't initialised.
 
 const IGNORE = ["escapeAddonURI", "shouldAutoUpdate", "getStartupChanges",
                 "addTypeListener", "removeTypeListener",
                 "addAddonListener", "removeAddonListener",
                 "addInstallListener", "removeInstallListener",
-                "addManagerListener", "removeManagerListener"];
+                "addManagerListener", "removeManagerListener",
+                "mapURIToAddonID"];
 
 const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
                         "AddonScreenshot", "AddonType", "startup", "shutdown",
                         "registerProvider", "unregisterProvider",
                         "addStartupChange", "removeStartupChange",
                         "recordTimestamp", "recordSimpleMeasure",
                         "getSimpleMeasures"];
 
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -182,16 +182,19 @@ skip-if = os == "android"
 [test_install_strictcompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_locale.js]
 [test_locked.js]
 [test_locked2.js]
 [test_locked_strictcompat.js]
 [test_manifest.js]
+[test_mapURIToAddonID.js]
+# Same as test_bootstrap.js
+skip-if = os == "android"
 [test_migrate1.js]
 [test_migrate2.js]
 [test_migrate3.js]
 [test_migrate4.js]
 [test_migrate5.js]
 [test_migrateAddonRepository.js]
 [test_onPropertyChanged_appDisabled.js]
 [test_permissions.js]