Merge inbound to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Sun, 20 May 2018 12:53:43 +0300
changeset 419052 226437245483e931aa1413c2db7668435f03bff9
parent 419044 e87851499d8f8f6d76112da9e15ba5cce2c3557e (current diff)
parent 419051 ccfe668f71ffded4b72485805b56fe8040bc0d9c (diff)
child 419053 d2b91476bebc48f9e89f9d3e6c7b33decb2ae941
child 419055 9d81ad889dcb4de350628c778d4b9d009c49ce2c
push id34022
push userrgurzau@mozilla.com
push dateSun, 20 May 2018 09:56:55 +0000
treeherdermozilla-central@226437245483 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone62.0a1
first release with
nightly linux32
226437245483 / 62.0a1 / 20180520100821 / files
nightly linux64
226437245483 / 62.0a1 / 20180520100821 / files
nightly mac
226437245483 / 62.0a1 / 20180520100821 / files
nightly win32
226437245483 / 62.0a1 / 20180520100821 / files
nightly win64
226437245483 / 62.0a1 / 20180520100821 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
dom/workers/test/extensions/bootstrap/bootstrap.js
dom/workers/test/extensions/bootstrap/install.rdf
dom/workers/test/extensions/bootstrap/jar.mn
dom/workers/test/extensions/bootstrap/worker.js
dom/workers/test/extensions/moz.build
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -25,19 +25,16 @@ pref("browser.chromeURL","chrome://brows
 pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindow.xul");
 
 // Enables some extra Extension System Logging (can reduce performance)
 pref("extensions.logging.enabled", false);
 
 // Disables strict compatibility, making addons compatible-by-default.
 pref("extensions.strictCompatibility", false);
 
-// Specifies a minimum maxVersion an addon needs to say it's compatible with
-// for it to be compatible by default.
-pref("extensions.minCompatibleAppVersion", "4.0");
 // Temporary preference to forcibly make themes more safe with Australis even if
 // extensions.checkCompatibility=false has been set.
 pref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "29.0a1");
 
 pref("xpinstall.customConfirmationUI", true);
 pref("extensions.webextPermissionPrompts", true);
 pref("extensions.webextOptionalPermissionPrompts", true);
 
deleted file mode 100644
--- a/dom/workers/test/extensions/bootstrap/bootstrap.js
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-function testForExpectedSymbols(stage, data) {
-  const expectedSymbols = [ "Worker", "ChromeWorker" ];
-  for (var symbol of expectedSymbols) {
-    Services.prefs.setBoolPref("workertest.bootstrap." + stage + "." + symbol,
-                               symbol in this);
-  }
-}
-
-var gWorkerAndCallback = {
-  _ensureStarted: function() {
-    if (!this._worker) {
-      throw new Error("Not yet started!");
-    }
-  },
-
-  start: function(data) {
-    if (!this._worker) {
-      this._worker = new Worker("chrome://workerbootstrap/content/worker.js");
-      this._worker.onerror = function(event) {
-        Cu.reportError(event.message);
-        event.preventDefault();
-      };
-    }
-  },
-
-  stop: function() {
-    if (this._worker) {
-      this._worker.terminate();
-      delete this._worker;
-    }
-  },
-
-  set callback(val) {
-    this._ensureStarted();
-    var callback = val.QueryInterface(Ci.nsIObserver);
-    if (this._callback != callback) {
-      if (callback) {
-        this._worker.onmessage = function(event) {
-          callback.observe(this, event.type, event.data);
-        };
-        this._worker.onerror = function(event) {
-          callback.observe(this, event.type, event.message);
-          event.preventDefault();
-        };
-      }
-      else {
-        this._worker.onmessage = null;
-        this._worker.onerror = null;
-      }
-      this._callback = callback;
-    }
-  },
-
-  get callback() {
-    return this._callback;
-  },
-
-  postMessage: function(data) {
-    this._ensureStarted();
-    this._worker.postMessage(data);
-  },
-
-  terminate: function() {
-    this._ensureStarted();
-    this._worker.terminate();
-    delete this._callback;
-  }
-};
-
-function WorkerTestBootstrap() {
-}
-WorkerTestBootstrap.prototype = {
-  observe: function(subject, topic, data) {
-
-    gWorkerAndCallback.callback = subject;
-
-    switch (topic) {
-      case "postMessage":
-        gWorkerAndCallback.postMessage(data);
-        break;
-
-      case "terminate":
-        gWorkerAndCallback.terminate();
-        break;
-
-      default:
-        throw new Error("Unknown worker command");
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver])
-};
-
-var gFactory = {
-  register: function() {
-    var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-
-    var classID = Components.ID("{36b5df0b-8dcf-4aa2-9c45-c51d871295f9}");
-    var description = "WorkerTestBootstrap";
-    var contractID = "@mozilla.org/test/workertestbootstrap;1";
-    var factory = XPCOMUtils._getFactory(WorkerTestBootstrap);
-
-    registrar.registerFactory(classID, description, contractID, factory);
-
-    this.unregister = function() {
-      registrar.unregisterFactory(classID, factory);
-      delete this.unregister;
-    };
-  }
-};
-
-function install(data, reason) {
-  testForExpectedSymbols("install");
-}
-
-function startup(data, reason) {
-  testForExpectedSymbols("startup");
-  gFactory.register();
-  gWorkerAndCallback.start(data);
-}
-
-function shutdown(data, reason) {
-  testForExpectedSymbols("shutdown");
-  gWorkerAndCallback.stop();
-  gFactory.unregister();
-}
-
-function uninstall(data, reason) {
-  testForExpectedSymbols("uninstall");
-}
deleted file mode 100644
--- a/dom/workers/test/extensions/bootstrap/install.rdf
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0"?>
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-
-  <Description about="urn:mozilla:install-manifest">
-    <em:name>WorkerTestBootstrap</em:name>
-    <em:description>Worker functions for use in testing.</em:description>
-    <em:creator>Mozilla</em:creator>
-    <em:version>2016.03.09</em:version>
-    <em:id>workerbootstrap-test@mozilla.org</em:id>
-    <em:type>2</em:type>
-    <em:bootstrap>true</em:bootstrap>
-    <em:targetApplication>
-      <Description>
-        <!-- Firefox -->
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>45.0</em:minVersion>
-        <em:maxVersion>*</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-    <em:targetApplication>
-      <Description>
-        <!-- Fennec -->
-        <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
-        <em:minVersion>45.0</em:minVersion>
-        <em:maxVersion>*</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-  </Description>
-</RDF>
deleted file mode 100644
--- a/dom/workers/test/extensions/bootstrap/jar.mn
+++ /dev/null
@@ -1,3 +0,0 @@
-workerbootstrap.jar:
-% content workerbootstrap %content/
-  content/worker.js (worker.js)
--- a/dom/workers/test/extensions/bootstrap/moz.build
+++ b/dom/workers/test/extensions/bootstrap/moz.build
@@ -1,20 +1,9 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # 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/.
 
-XPI_NAME = 'workerbootstrap'
-
-JAR_MANIFESTS += ['jar.mn']
-USE_EXTENSION_MANIFEST = True
-NO_JS_MANIFEST = True
-
-FINAL_TARGET_FILES += [
-    'bootstrap.js',
-    'install.rdf',
-]
-
 TEST_HARNESS_FILES.testing.mochitest.extensions += [
     'workerbootstrap-test@mozilla.org.xpi',
 ]
deleted file mode 100644
--- a/dom/workers/test/extensions/bootstrap/worker.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-onmessage = function(event) {
-  postMessage(event.data);
-}
index 2dab975db0c8293b5e63a1e7a14bc632d253e890..5c9bc9e3abe8f85d1813c2b0d838ee343dbf9909
GIT binary patch
literal 583
zc$^FHW@Zs#W?<l8Xl&l>6BOBJznYPOL7s(yft!JWAv3SIBrzvPuP7yLZ^*&E*#;te
zK8xyAuTC*&%VAQww=L@J+UcvZ&L68>5_ZsSo94~1zu#xI966h{)OO95ISKd9RNPb8
zxzGBG>@U`1UTe1%PMh$f&o%Jx(|?RVJ2&0_(O&F4b=hST)!=y<oBcO$(g}77_S$%J
z>kT`t<NOgfXInV^v*gr%v3|?mf4aN-<asp|UAg|Q+1DBQB=oCoMB<YNH9SY8ZFl9`
zNQLUI^3r-;asA;u?VC-dai;S_oZ~g`G5=n7*54rh?~}>b&RHF9w~B3R+{N9l!*!+4
z>&$(zt+y6jWSYcf`Cj>|-AA$TMGsVX3YPQ!t~vF*Q(R}lv?xi{o4HP(TBgh^Z)Em=
zccaAT@|?|&Ef2Q;*ZO>Drnt$wLIyY6nkwGrZua7|7G*^T$8%@p?i^2ivE)JG)(^_(
zZZz#XEMe=xyw*;kXS!p@fA%A7PnN_cJ8~XBntSolCdG!$bDr;MDVm$#wg0rx^;dDL
z^1T(GYAL$=9hg_}^I^~a`JId$KQFIl^}A5ay?+yT0N0Vtp#}50L*0z`Jmx*h%GhN-
z^FamgoOMwdF4HE8uhzJ?t4sZf)mL+d?thE{-i%E4%(!Azg#iK-8kRINpvEy5g92EJ
mkwJo?VVV&8$@iBT8_GF&!8DW~;LXYgQNzd(3#6|wf_MPdcluud
deleted file mode 100644
--- a/dom/workers/test/extensions/moz.build
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# 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/.
-
-DIRS += ['bootstrap']
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4894,17 +4894,16 @@ pref("browser.history.maxStateObjectSize
 
 pref("browser.meta_refresh_when_inactive.disabled", false);
 
 // XPInstall prefs
 pref("xpinstall.whitelist.required", true);
 // Only Firefox requires add-on signatures
 pref("xpinstall.signatures.required", false);
 pref("extensions.langpacks.signatures.required", false);
-pref("extensions.minCompatiblePlatformVersion", "2.0");
 pref("extensions.webExtensionsMinPlatformVersion", "42.0a1");
 pref("extensions.legacy.enabled", true);
 
 // Other webextensions prefs
 pref("extensions.webextensions.keepStorageOnUninstall", false);
 pref("extensions.webextensions.keepUuidOnUninstall", false);
 // Redirect basedomain used by identity api
 pref("extensions.webextensions.identity.redirectDomain", "extensions.allizom.org");
--- a/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
+++ b/python/mozbuild/mozbuild/codecoverage/lcov_rewriter.py
@@ -571,17 +571,16 @@ class UrlFinder(object):
                 else:
                     # We don't know how to handle this jar: path, so return it to the
                     # caller to make it print a warning.
                     return url_obj.path, None
 
                 dir_parts = parts[0].rsplit(app_name + '/', 1)
                 url = mozpath.normpath(mozpath.join(self.topobjdir, 'dist', 'bin', dir_parts[1].lstrip('/'), parts[1].lstrip('/')))
             elif '.xpi!' in url:
-                # e.g. file:///tmp/tmpMdo5gV.mozrunner/extensions/workerbootstrap-test@mozilla.org.xpi!/bootstrap.js
                 # This matching mechanism is quite brittle and based on examples seen in the wild.
                 # There's no rule to match the XPI name to the path in dist/xpi-stage.
                 parts = url_obj.path.split('.xpi!', 1)
                 addon_name = os.path.basename(parts[0])
                 if '-test@mozilla.org' in addon_name:
                     addon_name = addon_name[:-len('-test@mozilla.org')]
                 elif addon_name.endswith('@mozilla.org'):
                     addon_name = addon_name[:-len('@mozilla.org')]
--- a/python/mozbuild/mozbuild/test/codecoverage/test_lcov_rewrite.py
+++ b/python/mozbuild/mozbuild/test/codecoverage/test_lcov_rewrite.py
@@ -292,20 +292,16 @@ class TestUrlFinder(unittest.TestCase):
                 'dist/bin/browser/components/nsSessionStartup.js': [
                     'path2',
                     None
                 ],
                 'dist/bin/browser/features/firefox@getpocket.com/bootstrap.js': [
                     'path4',
                     None
                 ],
-                'dist/xpi-stage/workerbootstrap/bootstrap.js': [
-                    'path5',
-                    None
-                ],
                 'dist/bin/modules/osfile/osfile_async_worker.js': [
                     'toolkit/components/osfile/modules/osfile_async_worker.js',
                     None
                 ],
                 'dist/bin/browser/features/activity-stream@mozilla.org/chrome/content/lib/': [
                     'browser/extensions/activity-stream/lib/*',
                     None
                 ],
@@ -335,17 +331,16 @@ class TestUrlFinder(unittest.TestCase):
     def test_jar_paths(self):
         app_name = buildconfig.substs.get('MOZ_APP_NAME')
         omnijar_name = buildconfig.substs.get('OMNIJAR_NAME')
 
         paths = [
             ('jar:file:///home/worker/workspace/build/application/' + app_name + '/' + omnijar_name + '!/components/MainProcessSingleton.js', 'path1'),
             ('jar:file:///home/worker/workspace/build/application/' + app_name + '/browser/' + omnijar_name + '!/components/nsSessionStartup.js', 'path2'),
             ('jar:file:///home/worker/workspace/build/application/' + app_name + '/browser/features/firefox@getpocket.com.xpi!/bootstrap.js', 'path4'),
-            ('jar:file:///tmp/tmpMdo5gV.mozrunner/extensions/workerbootstrap-test@mozilla.org.xpi!/bootstrap.js', 'path5'),
         ]
 
         url_finder = lcov_rewriter.UrlFinder(self._chrome_map_file, '', '', [])
         for path, expected in paths:
             self.assertEqual(url_finder.rewrite_url(path)[0], expected)
 
     def test_wrong_scheme_paths(self):
         app_name = buildconfig.substs.get('MOZ_APP_NAME')
--- a/testing/mochitest/runrobocop.py
+++ b/testing/mochitest/runrobocop.py
@@ -234,17 +234,16 @@ class RobocopTestRunner(MochitestDesktop
         self.options.extraPrefs.append('browser.snippets.enabled=false')
         self.options.extraPrefs.append('extensions.autoupdate.enabled=false')
 
         # Override the telemetry init delay for integration testing.
         self.options.extraPrefs.append('toolkit.telemetry.initDelay=1')
 
         self.options.extensionsToExclude.extend([
             'mochikit@mozilla.org',
-            'workerbootstrap-test@mozilla.org.xpi',
             'indexedDB-test@mozilla.org.xpi',
         ])
 
         manifest = MochitestDesktop.buildProfile(self, self.options)
         self.localProfile = self.options.profilePath
         self.log.debug("Profile created at %s" % self.localProfile)
         # some files are not needed for robocop; save time by not pushing
         os.remove(os.path.join(self.localProfile, 'userChrome.css'))
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/html/browsers/history/the-history-interface/history_go_zero.html.ini
@@ -0,0 +1,3 @@
+[history_go_zero.html]
+  disabled:
+    if debug and (os == "linux"): https://bugzilla.mozilla.org/show_bug.cgi?id=1217701
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -370,54 +370,38 @@ var AddonTestUtils = {
         let {value} = Object.getOwnPropertyDescriptor(FileTestUtils,
                                                       "_globalTemporaryDirectory");
         if (value) {
           ignoreEntries.add(value.leafName);
         }
       }
 
       // Check that the temporary directory is empty
-      var dirEntries = this.tempDir.directoryEntries
-                           .QueryInterface(Ci.nsIDirectoryEnumerator);
       var entries = [];
-      while (dirEntries.hasMoreElements()) {
-        let {leafName} = dirEntries.nextFile;
+      for (let {leafName} of this.iterDirectory(this.tempDir)) {
         if (!ignoreEntries.has(leafName)) {
           entries.push(leafName);
         }
       }
       if (entries.length)
         throw new Error(`Found unexpected files in temporary directory: ${entries.join(", ")}`);
 
-      dirEntries.close();
-
       try {
         appDirForAddons.remove(true);
       } catch (ex) {
         testScope.info(`Got exception removing addon app dir: ${ex}`);
       }
 
       // ensure no leftover files in the system addon upgrade location
       let featuresDir = this.profileDir.clone();
       featuresDir.append("features");
       // upgrade directories will be in UUID folders under features/
-      let systemAddonDirs = [];
-      if (featuresDir.exists()) {
-        let featuresDirEntries = featuresDir.directoryEntries
-                                            .QueryInterface(Ci.nsIDirectoryEnumerator);
-        while (featuresDirEntries.hasMoreElements()) {
-          let entry = featuresDirEntries.getNext();
-          entry.QueryInterface(Ci.nsIFile);
-          systemAddonDirs.push(entry);
-        }
-
-        systemAddonDirs.map(dir => {
-          dir.append("stage");
-          pathShouldntExist(dir);
-        });
+      for (let dir of this.iterDirectory(featuresDir)) {
+        dir.append("stage");
+        pathShouldntExist(dir);
       }
 
       // ensure no leftover files in the user addon location
       let testDir = this.profileDir.clone();
       testDir.append("extensions");
       testDir.append("trash");
       pathShouldntExist(testDir);
 
@@ -443,16 +427,43 @@ var AddonTestUtils = {
         this.tempDir.remove(true);
       } catch (e) {
         Cu.reportError(e);
       }
     });
   },
 
   /**
+   * Iterates over the entries in a given directory.
+   *
+   * Fails silently if the given directory does not exist.
+   *
+   * @param {nsIFile} dir
+   *        Directory to iterate.
+   */
+  * iterDirectory(dir) {
+    let dirEnum;
+    try {
+      dirEnum = dir.directoryEntries;
+      let file;
+      while ((file = dirEnum.nextFile)) {
+        yield file;
+      }
+    } catch (e) {
+      if (dir.exists()) {
+        Cu.reportError(e);
+      }
+    } finally {
+      if (dirEnum) {
+        dirEnum.close();
+      }
+    }
+  },
+
+  /**
    * Creates a new HttpServer for testing, and begins listening on the
    * specified port. Automatically shuts down the server when the test
    * unit ends.
    *
    * @param {object} [options = {}]
    *        The options object.
    * @param {integer} [options.port = -1]
    *        The port to listen on. If omitted, listen on a random
@@ -1173,21 +1184,18 @@ var AddonTestUtils = {
    * @param {nsIFile} ext A file pointing to either the packed extension or its unpacked directory.
    * @param {number} time The time to which we set the lastModifiedTime of the extension
    *
    * @deprecated Please use promiseSetExtensionModifiedTime instead
    */
   setExtensionModifiedTime(ext, time) {
     ext.lastModifiedTime = time;
     if (ext.isDirectory()) {
-      let entries = ext.directoryEntries
-                       .QueryInterface(Ci.nsIDirectoryEnumerator);
-      while (entries.hasMoreElements())
-        this.setExtensionModifiedTime(entries.nextFile, time);
-      entries.close();
+      for (let file of this.iterDirectory(ext))
+        this.setExtensionModifiedTime(file, time);
     }
   },
 
   async promiseSetExtensionModifiedTime(path, time) {
     await OS.File.setDates(path, time, time);
 
     let iterator = new OS.File.DirectoryIterator(path);
     try {
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -116,17 +116,17 @@ const COMPATIBLE_BY_DEFAULT_TYPES = {
   "webextension-dictionary": true,
 };
 
 // Properties that exist in the extension manifest
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
 
 // Properties to save in JSON file
-const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
+const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type",
                           "updateURL", "optionsURL",
                           "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "installDate",
                           "updateDate", "applyBackgroundUpdates", "path",
                           "skinnable", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
@@ -264,22 +264,25 @@ class AddonInternal {
     this.hasEmbeddedWebExtension = false;
 
     if (addonData) {
       if (addonData.descriptor && !addonData.path) {
         addonData.path = descriptorToPath(addonData.descriptor);
       }
 
       copyProperties(addonData, PROP_JSON_FIELDS, this);
+      this.location = addonData.location;
 
       if (!this.dependencies)
         this.dependencies = [];
       Object.freeze(this.dependencies);
 
-      this.addedToDatabase();
+      if (this.location) {
+        this.addedToDatabase();
+      }
 
       if (!addonData._sourceBundle) {
         throw new Error("Expected passed argument to contain a path");
       }
 
       this._sourceBundle = addonData._sourceBundle;
     }
   }
@@ -287,24 +290,17 @@ class AddonInternal {
   get wrapper() {
     if (!this._wrapper) {
       this._wrapper = new AddonWrapper(this);
     }
     return this._wrapper;
   }
 
   addedToDatabase() {
-    if (this._installLocation) {
-      this.location = this._installLocation.name;
-    } else if (this.location) {
-      this._installLocation = XPIProvider.installLocationsByName[this.location];
-    }
-
-    this._key = `${this.location}:${this.id}`;
-
+    this._key = `${this.location.name}:${this.id}`;
     this.inDatabase = true;
   }
 
   get selectedLocale() {
     if (this._selectedLocale)
       return this._selectedLocale;
 
     /**
@@ -352,17 +348,17 @@ class AddonInternal {
     return this._selectedLocale;
   }
 
   get providesUpdatesSecurely() {
     return !this.updateURL || this.updateURL.startsWith("https:");
   }
 
   get isCorrectlySigned() {
-    switch (this._installLocation.name) {
+    switch (this.location.name) {
       case KEY_APP_SYSTEM_ADDONS:
         // System add-ons must be signed by the system key.
         return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM;
 
       case KEY_APP_SYSTEM_DEFAULTS:
       case KEY_APP_TEMPORARY:
         // Temporary and built-in system add-ons do not require signing.
         return true;
@@ -381,16 +377,20 @@ class AddonInternal {
       return true;
     return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
   }
 
   get isCompatible() {
     return this.isCompatibleWith();
   }
 
+  get hidden() {
+    return this.location.isSystem;
+  }
+
   get disabled() {
     return (this.userDisabled || this.appDisabled || this.softDisabled);
   }
 
   get isPlatformCompatible() {
     if (this.targetPlatforms.length == 0)
       return true;
 
@@ -462,27 +462,16 @@ class AddonInternal {
       if (overrides) {
         let override = AddonRepository.findMatchingCompatOverride(this.version,
                                                                   overrides);
         if (override) {
           return false;
         }
       }
 
-      // Extremely old extensions should not be compatible by default.
-      let minCompatVersion;
-      if (app.id == Services.appinfo.ID)
-        minCompatVersion = XPIProvider.minCompatibleAppVersion;
-      else if (app.id == TOOLKIT_ID)
-        minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
-
-      if (minCompatVersion &&
-          Services.vc.compare(minCompatVersion, maxVersion) > 0)
-        return false;
-
       return Services.vc.compare(version, minVersion) >= 0;
     }
 
     return (Services.vc.compare(version, minVersion) >= 0) &&
            (Services.vc.compare(version, maxVersion) <= 0);
   }
 
   get matchingTargetApplication() {
@@ -566,17 +555,19 @@ class AddonInternal {
       if (this.inDatabase)
         XPIDatabase.updateAddonDisabledState(this);
       else
         this.appDisabled = !XPIDatabase.isUsableAddon(this);
     }
   }
 
   toJSON() {
-    return copyProperties(this, PROP_JSON_FIELDS);
+    let obj = copyProperties(this, PROP_JSON_FIELDS);
+    obj.location = this.location.name;
+    return obj;
   }
 
   /**
    * When an add-on install is pending its metadata will be cached in a file.
    * This method reads particular properties of that metadata that may be newer
    * than that in the extension manifest, like compatibility information.
    *
    * @param {Object} aObj
@@ -606,21 +597,21 @@ class AddonInternal {
         permissions |= AddonManager.PERM_CAN_ENABLE;
       } else if (this.type != "theme") {
         permissions |= AddonManager.PERM_CAN_DISABLE;
       }
     }
 
     // Add-ons that are in locked install locations, or are pending uninstall
     // cannot be upgraded or uninstalled
-    if (!this._installLocation.locked && !this.pendingUninstall) {
+    if (!this.location.locked && !this.pendingUninstall) {
       // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
-      let isSystem = this._installLocation.isSystem;
+      let isSystem = this.location.isSystem;
       // Add-ons that are installed by a file link cannot be upgraded.
-      if (!this._installLocation.isLinkedAddon(this.id) && !isSystem) {
+      if (!this.location.isLinkedAddon(this.id) && !isSystem) {
         permissions |= AddonManager.PERM_CAN_UPGRADE;
       }
 
       permissions |= AddonManager.PERM_CAN_UNINSTALL;
     }
 
     if (Services.policies &&
         !Services.policies.isAllowed(`modify-extension:${this.id}`)) {
@@ -665,17 +656,17 @@ AddonWrapper = class {
     return XPIInternal.getExternalType(addonFor(this).type);
   }
 
   get isWebExtension() {
     return isWebExtension(addonFor(this).type);
   }
 
   get temporarilyInstalled() {
-    return addonFor(this)._installLocation == XPIInternal.TemporaryInstallLocation;
+    return addonFor(this).location.isTemporary;
   }
 
   get aboutURL() {
     return this.isActive ? addonFor(this).aboutURL : null;
   }
 
   get optionsURL() {
     if (!this.isActive) {
@@ -838,18 +829,18 @@ AddonWrapper = class {
 
   get pendingUpgrade() {
     let addon = addonFor(this);
     return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null;
   }
 
   get scope() {
     let addon = addonFor(this);
-    if (addon._installLocation)
-      return addon._installLocation.scope;
+    if (addon.location)
+      return addon.location.scope;
 
     return AddonManager.SCOPE_PROFILE;
   }
 
   get pendingOperations() {
     let addon = addonFor(this);
     let pending = 0;
     if (!(addon.inDatabase)) {
@@ -958,50 +949,50 @@ AddonWrapper = class {
       addon.softDisabled = val;
     }
 
     return val;
   }
 
   get hidden() {
     let addon = addonFor(this);
-    if (addon._installLocation.name == KEY_APP_TEMPORARY)
+    if (addon.location.isTemporary)
       return false;
 
-    return addon._installLocation.isSystem;
+    return addon.location.isSystem;
   }
 
   get isSystem() {
     let addon = addonFor(this);
-    return addon._installLocation.isSystem;
+    return addon.location.isSystem;
   }
 
   // Returns true if Firefox Sync should sync this addon. Only addons
   // in the profile install location are considered syncable.
   get isSyncable() {
     let addon = addonFor(this);
-    return (addon._installLocation.name == KEY_APP_PROFILE);
+    return (addon.location.name == KEY_APP_PROFILE);
   }
 
   get userPermissions() {
     return addonFor(this).userPermissions;
   }
 
   isCompatibleWith(aAppVersion, aPlatformVersion) {
     return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
   }
 
   uninstall(alwaysAllowUndo) {
     let addon = addonFor(this);
-    XPIProvider.uninstallAddon(addon, alwaysAllowUndo);
+    XPIInstall.uninstallAddon(addon, alwaysAllowUndo);
   }
 
   cancelUninstall() {
     let addon = addonFor(this);
-    XPIProvider.cancelUninstallAddon(addon);
+    XPIInstall.cancelUninstallAddon(addon);
   }
 
   findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
     new UpdateChecker(addonFor(this), aListener, aReason, aAppVersion, aPlatformVersion);
   }
 
   // Returns true if there was an update in progress, false if there was no update to cancel
   cancelUpdate() {
@@ -1280,16 +1271,19 @@ function _filterDB(addonDB, aFilter) {
 
 this.XPIDatabase = {
   // true if the database connection has been opened
   initialized: false,
   // The database file
   jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true),
   rebuildingDatabase: false,
   syncLoadingDB: false,
+  // Add-ons from the database in locations which are no longer
+  // supported.
+  orphanedAddons: [],
 
   _saveTask: null,
 
   // Saved error object if we fail to read an existing database
   _loadError: null,
 
   // Saved error object if we fail to save the database
   _saveError: null,
@@ -1368,17 +1362,17 @@ this.XPIDatabase = {
     if (!this.addonDB) {
       // We never loaded the database?
       throw new Error("Attempt to save database without loading it first");
     }
 
     let toSave = {
       schemaVersion: DB_SCHEMA,
       addons: Array.from(this.addonDB.values())
-                   .filter(addon => addon.location != KEY_APP_TEMPORARY),
+                   .filter(addon => !addon.location.isTemporary),
     };
     return toSave;
   },
 
   /**
    * Synchronously loads the database, by running the normal async load
    * operation with idle dispatch disabled, and spinning the event loop
    * until it finishes.
@@ -1442,19 +1436,24 @@ this.XPIDatabase = {
             loadedAddon.path = descriptorToPath(loadedAddon.descriptor);
           }
           loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
         } catch (e) {
           // We can fail here when the path is invalid, usually from the
           // wrong OS
           logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e);
         }
+        loadedAddon.location = XPIStates.getLocation(loadedAddon.location);
 
         let newAddon = new AddonInternal(loadedAddon);
-        addonDB.set(newAddon._key, newAddon);
+        if (loadedAddon.location) {
+          addonDB.set(newAddon._key, newAddon);
+        } else {
+          this.orphanedAddons.push(newAddon);
+        }
       });
 
       parseTimer.done();
       this.addonDB = addonDB;
       logger.debug("Successfully read XPI database");
       this.initialized = true;
     } catch (e) {
       // If we catch and log a SyntaxError from the JSON
@@ -1782,17 +1781,17 @@ this.XPIDatabase = {
   /**
    * Asynchronously get all the add-ons in a particular install location.
    *
    * @param {string} aLocation
    *        The name of the install location
    * @returns {Promise<Array<AddonInternal>>}
    */
   getAddonsInLocation(aLocation) {
-    return this.getAddonList(aAddon => aAddon._installLocation.name == aLocation);
+    return this.getAddonList(aAddon => aAddon.location.name == aLocation);
   },
 
   /**
    * Asynchronously gets the add-on with the specified ID that is visible.
    *
    * @param {string} aId
    *        The ID of the add-on to retrieve
    * @returns {Promise<AddonInternal?>}
@@ -1906,22 +1905,22 @@ this.XPIDatabase = {
    *
    * @returns {boolean} Whether the addon should be disabled for being legacy
    */
   isDisabledLegacy(addon) {
     return (!AddonSettings.ALLOW_LEGACY_EXTENSIONS &&
             LEGACY_TYPES.has(addon.type) &&
 
             // Legacy add-ons are allowed in the system location.
-            !addon._installLocation.isSystem &&
+            !addon.location.isSystem &&
 
             // Legacy extensions may be installed temporarily in
             // non-release builds.
             !(AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
-              addon._installLocation.name == KEY_APP_TEMPORARY) &&
+              addon.location.isTemporary) &&
 
             // Properly signed legacy extensions are allowed.
             addon.signedState !== AddonManager.SIGNEDSTATE_PRIVILEGED);
   },
 
   /**
    * Calculates whether an add-on should be appDisabled or not.
    *
@@ -2051,19 +2050,19 @@ this.XPIDatabase = {
    *        The AddonInternal being removed
    */
   removeAddonMetadata(aAddon) {
     this.addonDB.delete(aAddon._key);
     this.saveChanges();
   },
 
   updateXPIStates(addon) {
-    let xpiState = XPIStates.getAddon(addon.location, addon.id);
-    if (xpiState) {
-      xpiState.syncWithDB(addon);
+    let state = addon.location && addon.location.get(addon.id);
+    if (state) {
+      state.syncWithDB(addon);
       XPIStates.save();
     }
   },
 
   /**
    * Synchronously marks a AddonInternal as visible marking all other
    * instances with the same ID as not visible.
    *
@@ -2087,17 +2086,17 @@ this.XPIDatabase = {
   },
 
   /**
    * Synchronously marks a given add-on ID visible in a given location,
    * instances with the same ID as not visible.
    *
    * @param {string} aId
    *        The ID of the add-on to make visible
-   * @param {InstallLocation} aLocation
+   * @param {XPIStateLocation} aLocation
    *        The location in which to make the add-on visible.
    * @returns {AddonInternal?}
    *        The add-on instance which was marked visible, if any.
    */
   makeAddonLocationVisible(aId, aLocation) {
     logger.debug(`Make addon ${aId} visible in location ${aLocation}`);
     let result;
     for (let [, addon] of this.addonDB) {
@@ -2271,25 +2270,17 @@ this.XPIDatabase = {
     // If the add-on is not visible or the add-on is not changing state then
     // there is no need to do anything else
     if (!aAddon.visible || (wasDisabled == isDisabled))
       return undefined;
 
     // Flag that active states in the database need to be updated on shutdown
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
-    // Sync with XPIStates.
-    let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
-    if (xpiState) {
-      xpiState.syncWithDB(aAddon);
-      XPIStates.save();
-    } else {
-      // There should always be an xpiState
-      logger.warn("No XPIState for ${id} in ${location}", aAddon);
-    }
+    this.updateXPIStates(aAddon);
 
     // Have we just gone back to the current state?
     if (isDisabled != aAddon.active) {
       AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
     } else {
       if (isDisabled) {
         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
       } else {
@@ -2307,21 +2298,17 @@ this.XPIDatabase = {
         AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
       }
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type)) {
       if (!isDisabled) {
         AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type);
-
-        if (xpiState) {
-          xpiState.syncWithDB(aAddon);
-          XPIStates.save();
-        }
+        this.updateXPIStates(aAddon);
       } else if (isDisabled && !aBecauseSelecting) {
         AddonManagerPrivate.notifyAddonChanged(null, "theme");
       }
     }
 
     return isDisabled;
   },
 
@@ -2354,21 +2341,21 @@ this.XPIDatabaseReconcile = {
    *        The add-on map to flatten.
    * @param {string?} [hideLocation]
    *        An optional location from which to hide any add-ons.
    * @returns {Map<string, AddonInternal>}
    */
   flattenByID(addonMap, hideLocation) {
     let map = new Map();
 
-    for (let installLocation of XPIProvider.installLocations) {
-      if (installLocation.name == hideLocation)
+    for (let loc of XPIStates.locations()) {
+      if (loc.name == hideLocation)
         continue;
 
-      let locationMap = addonMap.get(installLocation.name);
+      let locationMap = addonMap.get(loc.name);
       if (!locationMap)
         continue;
 
       for (let [id, addon] of locationMap) {
         if (!map.has(id))
           map.set(id, addon);
       }
     }
@@ -2406,17 +2393,17 @@ this.XPIDatabaseReconcile = {
   /**
    * Called to add the metadata for an add-on in one of the install locations
    * to the database. This can be called in three different cases. Either an
    * add-on has been dropped into the location from outside of Firefox, or
    * an add-on has been installed through the application, or the database
    * has been upgraded or become corrupt and add-on data has to be reloaded
    * into it.
    *
-   * @param {InstallLocation} aInstallLocation
+   * @param {XPIStateLocation} aLocation
    *        The install location containing the add-on
    * @param {string} aId
    *        The ID of the add-on
    * @param {XPIState} aAddonState
    *        The new state of the add-on
    * @param {AddonInternal?} [aNewAddon]
    *        The manifest for the new add-on if it has already been loaded
    * @param {string?} [aOldAppVersion]
@@ -2424,19 +2411,19 @@ this.XPIDatabaseReconcile = {
    *        if it is a new profile or the version is unknown
    * @param {string?} [aOldPlatformVersion]
    *        The version of the platform last run with this profile or null
    *        if it is a new profile or the version is unknown
    * @returns {boolean}
    *        A boolean indicating if flushing caches is required to complete
    *        changing this add-on
    */
-  addMetadata(aInstallLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
+  addMetadata(aLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
               aOldPlatformVersion) {
-    logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
+    logger.debug(`New add-on ${aId} installed in ${aLocation.name}`);
 
     // We treat this is a new install if,
     //
     // a) It was explicitly registered as a staged install in the last
     //    session, or,
     // b) We're not currently migrating or rebuilding a corrupt database. In
     //    that case, we can assume this add-on was found during a routine
     //    directory scan.
@@ -2447,64 +2434,60 @@ this.XPIDatabaseReconcile = {
     let isDetectedInstall = isNewInstall && !aNewAddon;
 
     // Load the manifest if necessary and sanity check the add-on ID
     let unsigned;
     try {
       if (!aNewAddon) {
         // Load the manifest from the add-on.
         let file = new nsIFile(aAddonState.path);
-        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
+        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation);
       }
       // The add-on in the manifest should match the add-on ID.
       if (aNewAddon.id != aId) {
-        throw new Error("Invalid addon ID: expected addon ID " + aId +
-                        ", found " + aNewAddon.id + " in manifest");
+        throw new Error(`Invalid addon ID: expected addon ID ${aId}, found ${aNewAddon.id} in manifest`);
       }
 
       unsigned = XPIDatabase.mustSign(aNewAddon.type) && !aNewAddon.isCorrectlySigned;
       if (unsigned) {
           throw Error(`Extension ${aNewAddon.id} is not correctly signed`);
       }
     } catch (e) {
-      logger.warn("addMetadata: Add-on " + aId + " is invalid", e);
+      logger.warn(`addMetadata: Add-on ${aId} is invalid`, e);
 
       // Remove the invalid add-on from the install location if the install
       // location isn't locked
-      if (aInstallLocation.isLinkedAddon(aId))
+      if (aLocation.isLinkedAddon(aId))
         logger.warn("Not uninstalling invalid item because it is a proxy file");
-      else if (aInstallLocation.locked)
+      else if (aLocation.locked)
         logger.warn("Could not uninstall invalid item from locked install location");
       else if (unsigned && !isNewInstall)
         logger.warn("Not uninstalling existing unsigned add-on");
       else
-        aInstallLocation.uninstallAddon(aId);
+        aLocation.installer.uninstallAddon(aId);
       return null;
     }
 
     // Update the AddonInternal properties.
     aNewAddon.installDate = aAddonState.mtime;
     aNewAddon.updateDate = aAddonState.mtime;
 
     // Assume that add-ons in the system add-ons install location aren't
     // foreign and should default to enabled.
-    aNewAddon.foreignInstall = isDetectedInstall &&
-                               aInstallLocation.name != KEY_APP_SYSTEM_ADDONS &&
-                               aInstallLocation.name != KEY_APP_SYSTEM_DEFAULTS;
+    aNewAddon.foreignInstall = isDetectedInstall && !aLocation.isSystem;
 
     // appDisabled depends on whether the add-on is a foreignInstall so update
     aNewAddon.appDisabled = !XPIDatabase.isUsableAddon(aNewAddon);
 
     if (isDetectedInstall && aNewAddon.foreignInstall) {
       // If the add-on is a foreign install and is in a scope where add-ons
       // that were dropped in should default to disabled then disable it
       let disablingScopes = Services.prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
-      if (aInstallLocation.scope & disablingScopes) {
-        logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in "
-            + aInstallLocation.name);
+      if (aLocation.scope & disablingScopes) {
+        logger.warn(`Disabling foreign installed add-on ${aNewAddon.id} in ${aLocation.name}`);
         aNewAddon.userDisabled = true;
         aNewAddon.seen = false;
       }
     }
 
     return XPIDatabase.addToDatabase(aNewAddon, aAddonState.path);
   },
 
@@ -2512,57 +2495,59 @@ this.XPIDatabaseReconcile = {
    * Called when an add-on has been removed.
    *
    * @param {AddonInternal} aOldAddon
    *        The AddonInternal as it appeared the last time the application
    *        ran
    */
   removeMetadata(aOldAddon) {
     // This add-on has disappeared
-    logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
+    logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location.name);
     XPIDatabase.removeAddonMetadata(aOldAddon);
   },
 
   /**
    * Updates an add-on's metadata and determines. This is called when either the
    * add-on's install directory path or last modified time has changed.
    *
-   * @param {InstallLocation} aInstallLocation
+   * @param {XPIStateLocation} aLocation
    *        The install location containing the add-on
    * @param {AddonInternal} aOldAddon
    *        The AddonInternal as it appeared the last time the application
    *        ran
    * @param {XPIState} aAddonState
    *        The new state of the add-on
    * @param {AddonInternal?} [aNewAddon]
    *        The manifest for the new add-on if it has already been loaded
    * @returns {boolean?}
    *        A boolean indicating if flushing caches is required to complete
    *        changing this add-on
    */
-  updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
-    logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
+  updateMetadata(aLocation, aOldAddon, aAddonState, aNewAddon) {
+    logger.debug(`Add-on ${aOldAddon.id} modified in ${aLocation.name}`);
 
     try {
       // If there isn't an updated install manifest for this add-on then load it.
       if (!aNewAddon) {
         let file = new nsIFile(aAddonState.path);
-        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation, aOldAddon);
+        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation, aOldAddon);
       }
 
       // The ID in the manifest that was loaded must match the ID of the old
       // add-on.
       if (aNewAddon.id != aOldAddon.id)
-        throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id);
+        throw new Error(`Incorrect id in install manifest for existing add-on ${aOldAddon.id}`);
     } catch (e) {
-      logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
+      logger.warn(`updateMetadata: Add-on ${aOldAddon.id} is invalid`, e);
+
       XPIDatabase.removeAddonMetadata(aOldAddon);
-      XPIStates.removeAddon(aOldAddon.location, aOldAddon.id);
-      if (!aInstallLocation.locked)
-        aInstallLocation.uninstallAddon(aOldAddon.id);
+      aOldAddon.location.removeAddon(aOldAddon.id);
+
+      if (!aLocation.locked)
+        aLocation.installer.uninstallAddon(aOldAddon.id);
       else
         logger.warn("Could not uninstall invalid item from locked install location");
 
       return null;
     }
 
     // Set the additional properties on the new AddonInternal
     aNewAddon.updateDate = aAddonState.mtime;
@@ -2570,62 +2555,62 @@ this.XPIDatabaseReconcile = {
     // Update the database
     return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.path);
   },
 
   /**
    * Updates an add-on's path for when the add-on has moved in the
    * filesystem but hasn't changed in any other way.
    *
-   * @param {InstallLocation} aInstallLocation
+   * @param {XPIStateLocation} aLocation
    *        The install location containing the add-on
    * @param {AddonInternal} aOldAddon
    *        The AddonInternal as it appeared the last time the application
    *        ran
    * @param {XPIState} aAddonState
    *        The new state of the add-on
    * @returns {AddonInternal}
    */
-  updatePath(aInstallLocation, aOldAddon, aAddonState) {
-    logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.path);
+  updatePath(aLocation, aOldAddon, aAddonState) {
+    logger.debug(`Add-on ${aOldAddon.id} moved to ${aAddonState.path}`);
     aOldAddon.path = aAddonState.path;
     aOldAddon._sourceBundle = new nsIFile(aAddonState.path);
 
     return aOldAddon;
   },
 
   /**
    * Called when no change has been detected for an add-on's metadata but the
    * application has changed so compatibility may have changed.
    *
-   * @param {InstallLocation} aInstallLocation
+   * @param {XPIStateLocation} aLocation
    *        The install location containing the add-on
    * @param {AddonInternal} aOldAddon
    *        The AddonInternal as it appeared the last time the application
    *        ran
    * @param {XPIState} aAddonState
    *        The new state of the add-on
    * @param {boolean} [aReloadMetadata = false]
    *        A boolean which indicates whether metadata should be reloaded from
    *        the addon manifests. Default to false.
    * @returns {AddonInternal}
    *        The new addon.
    */
-  updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aReloadMetadata) {
-    logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
+  updateCompatibility(aLocation, aOldAddon, aAddonState, aReloadMetadata) {
+    logger.debug(`Updating compatibility for add-on ${aOldAddon.id} in ${aLocation.name}`);
 
     let checkSigning = (aOldAddon.signedState === undefined &&
                         AddonSettings.ADDON_SIGNING &&
                         SIGNED_TYPES.has(aOldAddon.type));
 
     let manifest = null;
     if (checkSigning || aReloadMetadata) {
       try {
         let file = new nsIFile(aAddonState.path);
-        manifest = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
+        manifest = XPIInstall.syncLoadManifestFromFile(file, aLocation);
       } catch (err) {
         // If we can no longer read the manifest, it is no longer compatible.
         aOldAddon.brokenManifest = true;
         aOldAddon.appDisabled = true;
         return aOldAddon;
       }
     }
 
@@ -2654,17 +2639,17 @@ this.XPIDatabaseReconcile = {
     return aOldAddon;
   },
 
   /**
    * Returns true if this install location is part of the application
    * bundle. Add-ons in these locations are expected to change whenever
    * the application updates.
    *
-   * @param {InstallLocation} location
+   * @param {XPIStateLocation} location
    *        The install location to check.
    * @returns {boolean}
    *        True if this location is part of the application bundle.
    */
   isAppBundledLocation(location) {
     return (location.name == KEY_APP_GLOBAL ||
             location.name == KEY_APP_SYSTEM_DEFAULTS);
   },
@@ -2687,17 +2672,17 @@ this.XPIDatabaseReconcile = {
    *        The schema has changed and all add-on manifests should be re-read.
    * @returns {AddonInternal?}
    *        The updated AddonInternal object for the add-on, if one
    *        could be created.
    */
   updateExistingAddon(oldAddon, xpiState, newAddon, aUpdateCompatibility, aSchemaChange) {
     XPIDatabase.recordAddonTelemetry(oldAddon);
 
-    let installLocation = oldAddon._installLocation;
+    let installLocation = oldAddon.location;
 
     if (xpiState.mtime < oldAddon.updateDate) {
       XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
         mtime: xpiState.mtime,
         oldtime: oldAddon.updateDate
       });
     }
 
@@ -2745,125 +2730,123 @@ this.XPIDatabaseReconcile = {
    * @param {boolean} aSchemaChange
    *        The schema has changed and all add-on manifests should be re-read.
    * @returns {boolean}
    *        A boolean indicating if a change requiring flushing the caches was
    *        detected
    */
   processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion,
                      aSchemaChange) {
-    let findManifest = (aInstallLocation, aId) => {
-      return (aManifests[aInstallLocation.name] &&
-              aManifests[aInstallLocation.name][aId]) || null;
+    let findManifest = (loc, id) => {
+      return (aManifests[loc.name] &&
+              aManifests[loc.name][id]) || null;
     };
 
     let addonExists = addon => addon._sourceBundle.exists();
 
     let previousAddons = new ExtensionUtils.DefaultMap(() => new Map());
     let currentAddons = new ExtensionUtils.DefaultMap(() => new Map());
 
     // Get the previous add-ons from the database and put them into maps by location
     for (let addon of XPIDatabase.getAddons()) {
-      previousAddons.get(addon.location).set(addon.id, addon);
+      previousAddons.get(addon.location.name).set(addon.id, addon);
     }
 
     // Keep track of add-ons whose blocklist status may have changed. We'll check this
     // after everything else.
     let addonsToCheckAgainstBlocklist = [];
 
     // Build the list of current add-ons into similar maps. When add-ons are still
     // present we re-use the add-on objects from the database and update their
     // details directly
     let addonStates = new Map();
-    for (let installLocation of XPIProvider.installLocations) {
-      let locationAddons = currentAddons.get(installLocation.name);
+    for (let location of XPIStates.locations()) {
+      let locationAddons = currentAddons.get(location.name);
 
       // Get all the on-disk XPI states for this location, and keep track of which
       // ones we see in the database.
-      let states = XPIStates.getLocation(installLocation.name) || new Map();
-      let dbAddons = previousAddons.get(installLocation.name) || new Map();
+      let dbAddons = previousAddons.get(location.name) || new Map();
       for (let [id, oldAddon] of dbAddons) {
         // Check if the add-on is still installed
-        let xpiState = states.get(id);
+        let xpiState = location.get(id);
         if (xpiState) {
           let newAddon = this.updateExistingAddon(oldAddon, xpiState,
-                                                  findManifest(installLocation, id),
+                                                  findManifest(location, id),
                                                   aUpdateCompatibility, aSchemaChange);
           if (newAddon) {
             locationAddons.set(newAddon.id, newAddon);
 
             // We need to do a blocklist check later, but the add-on may have changed by then.
             // Avoid storing the current copy and just get one when we need one instead.
             addonsToCheckAgainstBlocklist.push(newAddon.id);
           }
         } else {
           // The add-on is in the DB, but not in xpiState (and thus not on disk).
           this.removeMetadata(oldAddon);
         }
       }
 
-      for (let [id, xpiState] of states) {
+      for (let [id, xpiState] of location) {
         if (locationAddons.has(id))
           continue;
-        let newAddon = findManifest(installLocation, id);
-        let addon = this.addMetadata(installLocation, id, xpiState, newAddon,
+        let newAddon = findManifest(location, id);
+        let addon = this.addMetadata(location, id, xpiState, newAddon,
                                      aOldAppVersion, aOldPlatformVersion);
         if (addon) {
           locationAddons.set(addon.id, addon);
           addonStates.set(addon, xpiState);
         }
       }
     }
 
-    // Remove metadata for any add-ons in install locations that are no
-    // longer supported.
-    for (let [locationName, addons] of previousAddons) {
-      if (!currentAddons.has(locationName)) {
-        for (let oldAddon of addons.values())
-          this.removeMetadata(oldAddon);
-      }
-    }
-
     // Validate the updated system add-ons
     let hideLocation;
     {
-      let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+      let systemAddonLocation = XPIStates.getLocation(KEY_APP_SYSTEM_ADDONS);
       let addons = currentAddons.get(systemAddonLocation.name);
 
-      if (!systemAddonLocation.isValid(addons)) {
+      if (!systemAddonLocation.installer.isValid(addons)) {
         // Hide the system add-on updates if any are invalid.
         logger.info("One or more updated system add-ons invalid, falling back to defaults.");
         hideLocation = systemAddonLocation.name;
       }
     }
 
     // Apply startup changes to any currently-visible add-ons, and
     // uninstall any which were previously visible, but aren't anymore.
     let previousVisible = this.getVisibleAddons(previousAddons);
     let currentVisible = this.flattenByID(currentAddons, hideLocation);
 
+    for (let addon of XPIDatabase.orphanedAddons.splice(0)) {
+      if (addon.visible) {
+        previousVisible.set(addon.id, addon);
+      }
+    }
+
     for (let [id, addon] of currentVisible) {
       // If we have a stored manifest for the add-on, it came from the
       // startup data cache, and supersedes any previous XPIStates entry.
-      let xpiState = (!findManifest(addon._installLocation, id) &&
+      let xpiState = (!findManifest(addon.location, id) &&
                       addonStates.get(addon));
 
       this.applyStartupChange(addon, previousVisible.get(id), xpiState);
       previousVisible.delete(id);
     }
 
     for (let [id, addon] of previousVisible) {
-      if (addonExists(addon)) {
-        XPIInternal.BootstrapScope.get(addon).uninstall();
+      if (addon.location) {
+        if (addonExists(addon)) {
+          XPIInternal.BootstrapScope.get(addon).uninstall();
+        }
+        addon.location.removeAddon(id);
+        addon.visible = false;
+        addon.active = false;
       }
+
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
-      XPIStates.removeAddon(addon.location, id);
-
-      addon.visible = false;
-      addon.active = false;
     }
     if (previousVisible.size) {
       XPIInstall.flushChromeCaches();
     }
 
     // Finally update XPIStates to match everything
     for (let [locationName, locationAddons] of currentAddons) {
       for (let [id, addon] of locationAddons) {
@@ -2913,17 +2896,17 @@ this.XPIDatabaseReconcile = {
 
     let isActive = !currentAddon.disabled;
     let wasActive = previousAddon ? previousAddon.active : currentAddon.active;
 
     if (previousAddon) {
       if (previousAddon !== currentAddon) {
         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, id);
 
-        if (previousAddon._installLocation &&
+        if (previousAddon.location &&
             previousAddon._sourceBundle.exists() &&
             !previousAddon._sourceBundle.equals(currentAddon._sourceBundle)) {
           XPIInternal.BootstrapScope.get(previousAddon).update(
             currentAddon);
         } else {
           let reason = XPIInstall.newVersionReason(previousAddon.version, currentAddon.version);
           XPIInternal.BootstrapScope.get(currentAddon).install(
             reason, false, {oldVersion: previousAddon.version});
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -83,32 +83,32 @@ const PREF_DISTRO_ADDONS_PERMS        = 
 const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
 const PREF_XPI_ENABLED                = "xpinstall.enabled";
 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
 
-/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, KEY_APP_TEMPORARY, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isWebExtension */
+/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isWebExtension, iterDirectory */
 const XPI_INTERNAL_SYMBOLS = [
   "BOOTSTRAP_REASONS",
   "KEY_APP_SYSTEM_ADDONS",
   "KEY_APP_SYSTEM_DEFAULTS",
-  "KEY_APP_TEMPORARY",
   "PREF_BRANCH_INSTALLED_ADDON",
   "PREF_SYSTEM_ADDON_SET",
   "SIGNED_TYPES",
   "TEMPORARY_ADDON_SUFFIX",
   "TOOLKIT_ID",
   "XPI_PERMISSION",
   "XPIStates",
   "getExternalType",
   "isTheme",
   "isWebExtension",
+  "iterDirectory",
 ];
 
 for (let name of XPI_INTERNAL_SYMBOLS) {
   XPCOMUtils.defineLazyGetter(this, name, () => XPIInternal[name]);
 }
 
 /**
  * Returns a nsIFile instance for the given path, relative to the given
@@ -775,17 +775,17 @@ function generateTemporaryInstallID(aFil
   const sess = TEMP_INSTALL_ID_GEN_SESSION;
   hasher.update(sess, sess.length);
   hasher.update(data, data.length);
   let id = `${getHashStringForCrypto(hasher)}${TEMPORARY_ADDON_SUFFIX}`;
   logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`);
   return id;
 }
 
-var loadManifest = async function(aPackage, aInstallLocation, aOldAddon) {
+var loadManifest = async function(aPackage, aLocation, aOldAddon) {
   async function loadFromRDF(aUri) {
     let manifest = await aPackage.readString("install.rdf");
     let addon = await loadManifestFromRDF(aUri, manifest, aPackage);
 
     if (await aPackage.hasResource("icon.png")) {
       addon.icons[32] = "icon.png";
       addon.icons[48] = "icon.png";
     }
@@ -804,29 +804,29 @@ var loadManifest = async function(aPacka
   }
 
   let isWebExtension = entry == FILE_WEB_MANIFEST;
   let addon = isWebExtension ?
               await loadManifestFromWebManifest(aPackage.rootURI) :
               await loadFromRDF(aPackage.getURI("install.rdf"));
 
   addon._sourceBundle = aPackage.file;
-  addon._installLocation = aInstallLocation;
+  addon.location = aLocation;
 
   let {signedState, cert} = await aPackage.verifySignedState(addon);
   addon.signedState = signedState;
 
   if (isWebExtension && !addon.id) {
     if (cert) {
       addon.id = cert.commonName;
       if (!gIDTest.test(addon.id)) {
         throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
       }
     }
-    if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
+    if (!addon.id && aLocation.isTemporary) {
       addon.id = generateTemporaryInstallID(aPackage.file);
     }
   }
 
   await addon.updateBlocklistState({oldAddon: aOldAddon});
   addon.appDisabled = !XPIDatabase.isUsableAddon(addon);
 
   defineSyncGUID(addon);
@@ -834,42 +834,42 @@ var loadManifest = async function(aPacka
   return addon;
 };
 
 /**
  * Loads an add-on's manifest from the given file or directory.
  *
  * @param {nsIFile} aFile
  *        The file to load the manifest from.
- * @param {InstallLocation} aInstallLocation
+ * @param {XPIStateLocation} aLocation
  *        The install location the add-on is installed in, or will be
  *        installed to.
  * @param {AddonInternal?} aOldAddon
  *        The currently-installed add-on with the same ID, if one exist.
  *        This is used to migrate user settings like the add-on's
  *        disabled state.
  * @returns {AddonInternal}
  *        The parsed Addon object for the file's manifest.
  */
-var loadManifestFromFile = async function(aFile, aInstallLocation, aOldAddon) {
+var loadManifestFromFile = async function(aFile, aLocation, aOldAddon) {
   let pkg = Package.get(aFile);
   try {
-    let addon = await loadManifest(pkg, aInstallLocation, aOldAddon);
+    let addon = await loadManifest(pkg, aLocation, aOldAddon);
     return addon;
   } finally {
     pkg.close();
   }
 };
 
 /*
  * A synchronous method for loading an add-on's manifest. Do not use
  * this.
  */
-function syncLoadManifestFromFile(aFile, aInstallLocation, aOldAddon) {
-  return XPIInternal.awaitPromise(loadManifestFromFile(aFile, aInstallLocation, aOldAddon));
+function syncLoadManifestFromFile(aFile, aLocation, aOldAddon) {
+  return XPIInternal.awaitPromise(loadManifestFromFile(aFile, aLocation, aOldAddon));
 }
 
 function flushChromeCaches() {
   // Init this, so it will get the notification.
   Services.obs.notifyObservers(null, "startupcache-invalidate");
   // Flush message manager cached scripts
   Services.obs.notifyObservers(null, "message-manager-flush-caches");
   // Also dispatch this event to child processes
@@ -948,21 +948,21 @@ function getSignedStatus(aRv, aCert, aAd
       // Any other error indicates that either the add-on isn't signed or it
       // is signed by a signature that doesn't chain to the trusted root.
       return AddonManager.SIGNEDSTATE_UNKNOWN;
   }
 }
 
 function shouldVerifySignedState(aAddon) {
   // Updated system add-ons should always have their signature checked
-  if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS)
+  if (aAddon.location.name == KEY_APP_SYSTEM_ADDONS)
     return true;
 
   // We don't care about signatures for default system add-ons
-  if (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS)
+  if (aAddon.location.name == KEY_APP_SYSTEM_DEFAULTS)
     return false;
 
   // Otherwise only check signatures if signing is enabled and the add-on is one
   // of the signed types.
   return AddonSettings.ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type);
 }
 
 /**
@@ -1091,17 +1091,17 @@ function recursiveRemove(aFile) {
       throw e;
     }
   }
 
   // Use a snapshot of the directory contents to avoid possible issues with
   // iterating over a directory while removing files from it (the YAFFS2
   // embedded filesystem has this issue, see bug 772238), and to remove
   // normal files before their resource forks on OSX (see bug 733436).
-  let entries = getDirectoryEntries(aFile, true);
+  let entries = Array.from(iterDirectory(aFile));
   entries.forEach(recursiveRemove);
 
   try {
     aFile.remove(true);
   } catch (e) {
     logger.error("Failed to remove empty directory " + aFile.path, e);
     throw e;
   }
@@ -1267,54 +1267,16 @@ SafeInstallOperation.prototype = {
       }
     }
 
     while (this._createdDirs.length > 0)
       recursiveRemove(this._createdDirs.pop());
   }
 };
 
-/**
- * Gets a snapshot of directory entries.
- *
- * @param {nsIFile} aDir
- *        Directory to look at
- * @param {boolean} aSortEntries
- *        True to sort entries by filename
- * @returns {nsIFile[]}
- *        An files in the directory, or an empty array if aDir is not a
- *        readable directory.
- */
-function getDirectoryEntries(aDir, aSortEntries) {
-  let dirEnum;
-  try {
-    dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-    let entries = [];
-    while (dirEnum.hasMoreElements())
-      entries.push(dirEnum.nextFile);
-
-    if (aSortEntries) {
-      entries.sort(function(a, b) {
-        return a.path > b.path ? -1 : 1;
-      });
-    }
-
-    return entries;
-  } catch (e) {
-    if (aDir.exists()) {
-      logger.warn("Can't iterate directory " + aDir.path, e);
-    }
-    return [];
-  } finally {
-    if (dirEnum) {
-      dirEnum.close();
-    }
-  }
-}
-
 function getHashStringForCrypto(aCrypto) {
   // return the two-digit hexadecimal code for a byte
   let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
 
   // convert the binary hash data to a hex string.
   let binary = aCrypto.finish(false);
   let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)));
   return hash.join("").toLowerCase();
@@ -1323,17 +1285,17 @@ function getHashStringForCrypto(aCrypto)
 /**
  * Base class for objects that manage the installation of an addon.
  * This class isn't instantiated directly, see the derived classes below.
  */
 class AddonInstall {
   /**
    * Instantiates an AddonInstall.
    *
-   * @param {InstallLocation} installLocation
+   * @param {XPIStateLocation} installLocation
    *        The install location the add-on will be installed into
    * @param {nsIURL} url
    *        The nsIURL to get the add-on from. If this is an nsIFileURL then
    *        the add-on will not need to be downloaded
    * @param {Object} [options = {}]
    *        Additional options for the install
    * @param {string} [options.hash]
    *        An optional hash for the add-on
@@ -1347,17 +1309,17 @@ class AddonInstall {
    *        Optional icons for the add-on
    * @param {string} [options.version]
    *        An optional version for the add-on
    * @param {function(string) : Promise<void>} [options.promptHandler]
    *        A callback to prompt the user before installing.
    */
   constructor(installLocation, url, options = {}) {
     this.wrapper = new AddonInstallWrapper(this);
-    this.installLocation = installLocation;
+    this.location = installLocation;
     this.sourceURI = url;
 
     if (options.hash) {
       let hashSplit = options.hash.toLowerCase().split(":");
       this.originalHash = {
         algorithm: hashSplit[0],
         data: hashSplit[1]
       };
@@ -1459,19 +1421,19 @@ class AddonInstall {
       logger.debug("Cancelling download of " + this.sourceURI.spec);
       this.state = AddonManager.STATE_CANCELLED;
       XPIInstall.installs.delete(this);
       this._callInstallListeners("onDownloadCancelled");
       this.removeTemporaryFile();
       break;
     case AddonManager.STATE_INSTALLED:
       logger.debug("Cancelling install of " + this.addon.id);
-      let xpi = getFile(`${this.addon.id}.xpi`, this.installLocation.getStagingDir());
+      let xpi = getFile(`${this.addon.id}.xpi`, this.location.installer.getStagingDir());
       flushJarCache(xpi);
-      this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi"]);
+      this.location.installer.cleanStagingDir([this.addon.id, this.addon.id + ".xpi"]);
       this.state = AddonManager.STATE_CANCELLED;
       XPIInstall.installs.delete(this);
 
       if (this.existingAddon) {
         delete this.existingAddon.pendingUpgrade;
         this.existingAddon.pendingUpgrade = null;
       }
 
@@ -1481,17 +1443,17 @@ class AddonInstall {
       break;
     case AddonManager.STATE_POSTPONED:
       logger.debug(`Cancelling postponed install of ${this.addon.id}`);
       this.state = AddonManager.STATE_CANCELLED;
       XPIInstall.installs.delete(this);
       this._callInstallListeners("onInstallCancelled");
       this.removeTemporaryFile();
 
-      let stagingDir = this.installLocation.getStagingDir();
+      let stagingDir = this.location.installer.getStagingDir();
       let stagedAddon = stagingDir.clone();
 
       this.unstageInstall(stagedAddon);
     default:
       throw new Error("Cannot cancel install of " + this.sourceURI.spec +
                       " from this state (" + this.state + ")");
     }
   }
@@ -1521,29 +1483,29 @@ class AddonInstall {
   }
 
   /**
    * Removes the temporary file owned by this AddonInstall if there is one.
    */
   removeTemporaryFile() {
     // Only proceed if this AddonInstall owns its XPI file
     if (!this.ownsTempFile) {
-      this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
+      this.logger.debug(`removeTemporaryFile: ${this.sourceURI.spec} does not own temp file`);
       return;
     }
 
     try {
-      this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
-          this.file.path);
+      this.logger.debug(`removeTemporaryFile: ${this.sourceURI.spec} removing temp file ` +
+                        this.file.path);
       this.file.remove(true);
       this.ownsTempFile = false;
     } catch (e) {
-      this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
-          this.sourceURI.spec,
-          e);
+      this.logger.warn(`Failed to remove temporary file ${this.file.path} for addon ` +
+                       this.sourceURI.spec,
+                       e);
     }
   }
 
   /**
    * Updates the sourceURI and releaseNotesURI values on the Addon being
    * installed by this AddonInstall instance.
    */
   updateAddonURIs() {
@@ -1565,17 +1527,17 @@ class AddonInstall {
     try {
       pkg = Package.get(file);
     } catch (e) {
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     try {
       try {
-        this.addon = await loadManifest(pkg, this.installLocation, this.existingAddon);
+        this.addon = await loadManifest(pkg, this.location, this.existingAddon);
       } catch (e) {
         return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
       }
 
       if (!this.addon.id) {
         let err = new Error(`Cannot find id for addon ${file.path}`);
         return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
       }
@@ -1722,35 +1684,35 @@ class AddonInstall {
       this._callInstallListeners("onInstallCancelled");
       return;
     }
 
     // Find and cancel any pending installs for the same add-on in the same
     // install location
     for (let install of XPIInstall.installs) {
       if (install.state == AddonManager.STATE_INSTALLED &&
-          install.installLocation == this.installLocation &&
+          install.location == this.location &&
           install.addon.id == this.addon.id) {
         logger.debug(`Cancelling previous pending install of ${install.addon.id}`);
         install.cancel();
       }
     }
 
     let isUpgrade = this.existingAddon &&
-                    this.existingAddon._installLocation == this.installLocation;
+                    this.existingAddon.location == this.location;
 
     logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
     AddonManagerPrivate.callAddonListeners("onInstalling",
                                            this.addon.wrapper,
                                            false);
 
-    let stagedAddon = this.installLocation.getStagingDir();
+    let stagedAddon = this.location.installer.getStagingDir();
 
     (async () => {
-      await this.installLocation.requestStagingDir();
+      await this.location.installer.requestStagingDir();
 
       // remove any previously staged files
       await this.unstageInstall(stagedAddon);
 
       stagedAddon.append(`${this.addon.id}.xpi`);
 
       await this.stageInstall(false, stagedAddon, isUpgrade);
 
@@ -1759,30 +1721,30 @@ class AddonInstall {
 
       let install = () => {
         if (this.existingAddon && this.existingAddon.active && !isUpgrade) {
           XPIDatabase.updateAddonActive(this.existingAddon, false);
         }
 
         // Install the new add-on into its final location
         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
-        let file = this.installLocation.installAddon({
+        let file = this.location.installer.installAddon({
           id: this.addon.id,
           source: stagedAddon,
           existingAddonID
         });
 
         // Update the metadata in the database
         this.addon._sourceBundle = file;
         this.addon.visible = true;
 
         if (isUpgrade) {
           this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
                                                         file.path);
-          let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
+          let state = this.location.get(this.addon.id);
           if (state) {
             state.syncWithDB(this.addon, true);
           } else {
             logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
           }
         } else {
           this.addon.active = (this.addon.visible && !this.addon.disabled);
           this.addon = XPIDatabase.addToDatabase(this.addon, file.path);
@@ -1821,17 +1783,17 @@ class AddonInstall {
       this.state = AddonManager.STATE_INSTALL_FAILED;
       this.error = AddonManager.ERROR_FILE_ACCESS;
       XPIInstall.installs.delete(this);
       AddonManagerPrivate.callAddonListeners("onOperationCancelled",
                                              this.addon.wrapper);
       this._callInstallListeners("onInstallFailed");
     }).then(() => {
       this.removeTemporaryFile();
-      return this.installLocation.releaseStagingDir();
+      return this.location.installer.releaseStagingDir();
     });
   }
 
   /**
    * Stages an add-on for install.
    *
    * @param {boolean} restartRequired
    *        If true, the final installation will be deferred until the
@@ -1848,53 +1810,52 @@ class AddonInstall {
 
     await OS.File.copy(this.file.path, stagedAddon.path);
 
     if (restartRequired) {
       // Point the add-on to its extracted files as the xpi may get deleted
       this.addon._sourceBundle = stagedAddon;
 
       // Cache the AddonInternal as it may have updated compatibility info
-      XPIStates.getLocation(this.installLocation.name).stageAddon(this.addon.id,
-                                                                  this.addon.toJSON());
+      this.location.stageAddon(this.addon.id, this.addon.toJSON());
 
       logger.debug(`Staged install of ${this.addon.id} from ${this.sourceURI.spec} ready; waiting for restart.`);
       if (isUpgrade) {
         delete this.existingAddon.pendingUpgrade;
         this.existingAddon.pendingUpgrade = this.addon;
       }
     }
   }
 
   /**
    * Removes any previously staged upgrade.
    *
    * @param {nsIFile} stagingDir
    *        The staging directory from which to unstage the install.
    */
   async unstageInstall(stagingDir) {
-    XPIStates.getLocation(this.installLocation.name).unstageAddon(this.addon.id);
+    this.location.unstageAddon(this.addon.id);
 
     await removeAsync(getFile(this.addon.id, stagingDir));
 
     await removeAsync(getFile(`${this.addon.id}.xpi`, stagingDir));
   }
 
   /**
     * Postone a pending update, until restart or until the add-on resumes.
     *
     * @param {function} resumeFn
     *        A function for the add-on to run when resuming.
     */
   async postpone(resumeFn) {
     this.state = AddonManager.STATE_POSTPONED;
 
-    let stagingDir = this.installLocation.getStagingDir();
-
-    await this.installLocation.requestStagingDir();
+    let stagingDir = this.location.installer.getStagingDir();
+
+    await this.location.installer.requestStagingDir();
     await this.unstageInstall(stagingDir);
 
     let stagedAddon = getFile(`${this.addon.id}.xpi`, stagingDir);
 
     await this.stageInstall(true, stagedAddon, true);
 
     this._callInstallListeners("onInstallPostponed");
 
@@ -1916,17 +1877,17 @@ class AddonInstall {
             break;
           }
         },
       });
     }
     // Release the staging directory lock, but since the staging dir is populated
     // it will not be removed until resumed or installed by restart.
     // See also cleanStagingDir()
-    this.installLocation.releaseStagingDir();
+    this.location.installer.releaseStagingDir();
   }
 
   _callInstallListeners(event, ...args) {
     switch (event) {
       case "onDownloadCancelled":
       case "onDownloadFailed":
       case "onInstallCancelled":
       case "onInstallFailed":
@@ -2037,18 +1998,18 @@ var LocalAddonInstall = class extends Ad
     return super.install();
   }
 };
 
 var DownloadAddonInstall = class extends AddonInstall {
   /**
    * Instantiates a DownloadAddonInstall
    *
-   * @param {InstallLocation} installLocation
-   *        The InstallLocation the add-on will be installed into
+   * @param {XPIStateLocation} installLocation
+   *        The XPIStateLocation the add-on will be installed into
    * @param {nsIURL} url
    *        The nsIURL to get the add-on from
    * @param {Object} [options = {}]
    *        Additional options for the install
    * @param {string} [options.hash]
    *        An optional hash for the add-on
    * @param {AddonInternal} [options.existingAddon]
    *        The add-on this install will update if known
@@ -2447,20 +2408,20 @@ function createUpdate(aCallback, aAddon,
       existingAddon: aAddon,
       name: aAddon.selectedLocale.name,
       type: aAddon.type,
       icons: aAddon.icons,
       version: aUpdate.version,
     };
     let install;
     if (url instanceof Ci.nsIFileURL) {
-      install = new LocalAddonInstall(aAddon._installLocation, url, opts);
+      install = new LocalAddonInstall(aAddon.location, url, opts);
       await install.init();
     } else {
-      install = new DownloadAddonInstall(aAddon._installLocation, url, opts);
+      install = new DownloadAddonInstall(aAddon.location, url, opts);
     }
     try {
       if (aUpdate.updateInfoURL)
         install.releaseNotesURI = Services.io.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
     } catch (e) {
       // If the releaseNotesURI cannot be parsed then just ignore it.
     }
 
@@ -2683,17 +2644,17 @@ UpdateChecker.prototype = {
                           null :
                           await AddonRepository.getCompatibilityOverrides(this.addon.id);
 
     let update = await AUC.getNewestCompatibleUpdate(
       aUpdates, this.appVersion, this.platformVersion,
       ignoreMaxVersion, ignoreStrictCompat, compatOverrides);
 
     if (update && Services.vc.compare(this.addon.version, update.version) < 0
-        && !this.addon._installLocation.locked) {
+        && !this.addon.location.locked) {
       for (let currentInstall of XPIInstall.installs) {
         // Skip installs that don't match the available update
         if (currentInstall.existingAddon != this.addon ||
             currentInstall.version != update.version)
           continue;
 
         // If the existing install has not yet started downloading then send an
         // available update notification. If it is already downloading then
@@ -2741,62 +2702,75 @@ UpdateChecker.prototype = {
   }
 };
 
 /**
  * Creates a new AddonInstall to install an add-on from a local file.
  *
  * @param {nsIFile} file
  *        The file to install
- * @param {InstallLocation} location
+ * @param {XPIStateLocation} location
  *        The location to install to
  * @returns {Promise<AddonInstall>}
  *        A Promise that resolves with the new install object.
  */
 function createLocalInstall(file, location) {
   if (!location) {
-    location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+    location = XPIStates.getLocation(KEY_APP_PROFILE);
   }
   let url = Services.io.newFileURI(file);
 
   try {
     let install = new LocalAddonInstall(location, url);
     return install.init().then(() => install);
   } catch (e) {
     logger.error("Error creating install", e);
     XPIInstall.installs.delete(this);
     return Promise.resolve(null);
   }
 }
 
-// These are partial classes which contain the install logic for the
-// homonymous classes in XPIProvider.jsm. Those classes forward calls to
-// their install methods to these classes, with the `this` value set to
-// an instance the class as defined in XPIProvider.
-class DirectoryInstallLocation {}
-
-class MutableDirectoryInstallLocation extends DirectoryInstallLocation {
+class DirectoryInstaller {
+  constructor(location) {
+    this.location = location;
+
+    this._stagingDirLock = 0;
+    this._stagingDirPromise = null;
+  }
+
+  get name() {
+    return this.location.name;
+  }
+
+  get dir() {
+    return this.location.dir;
+  }
+  set dir(val) {
+    this.location.dir = val;
+    this.location.path = val.path;
+  }
+
   /**
    * Gets the staging directory to put add-ons that are pending install and
    * uninstall into.
    *
    * @returns {nsIFile}
    */
   getStagingDir() {
-    return getFile(DIR_STAGE, this._directory);
+    return getFile(DIR_STAGE, this.dir);
   }
 
   requestStagingDir() {
     this._stagingDirLock++;
 
     if (this._stagingDirPromise)
       return this._stagingDirPromise;
 
-    OS.File.makeDir(this._directory.path);
-    let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+    OS.File.makeDir(this.dir.path);
+    let stagepath = OS.Path.join(this.dir.path, DIR_STAGE);
     return this._stagingDirPromise = OS.File.makeDir(stagepath).catch((e) => {
       if (e instanceof OS.File.Error && e.becauseExists)
         return;
       logger.error("Failed to create staging directory", e);
       throw e;
     });
   }
 
@@ -2825,23 +2799,19 @@ class MutableDirectoryInstallLocation ex
     for (let name of aLeafNames) {
       let file = getFile(name, dir);
       recursiveRemove(file);
     }
 
     if (this._stagingDirLock > 0)
       return;
 
-    let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-    try {
-      if (dirEntries.nextFile)
-        return;
-    } finally {
-      dirEntries.close();
-    }
+    // eslint-disable-next-line no-unused-vars
+    for (let file of iterDirectory(dir))
+      return;
 
     try {
       setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
       dir.remove(false);
     } catch (e) {
       logger.warn("Failed to remove staging dir", e);
       // Failing to remove the staging directory is ignorable
     }
@@ -2851,17 +2821,17 @@ class MutableDirectoryInstallLocation ex
    * Returns a directory that is normally on the same filesystem as the rest of
    * the install location and can be used for temporarily storing files during
    * safe move operations. Calling this method will delete the existing trash
    * directory and its contents.
    *
    * @returns {nsIFile}
    */
   getTrashDir() {
-    let trashDir = getFile(DIR_TRASH, this._directory);
+    let trashDir = getFile(DIR_TRASH, this.dir);
     let trashDirExists = trashDir.exists();
     try {
       if (trashDirExists)
         recursiveRemove(trashDir);
       trashDirExists = false;
     } catch (e) {
       logger.warn("Failed to remove trash directory", e);
     }
@@ -2895,21 +2865,21 @@ class MutableDirectoryInstallLocation ex
    *        An nsIFile indicating where the add-on was installed to
    */
   installAddon({ id, source, existingAddonID, action = "move" }) {
     let trashDir = this.getTrashDir();
 
     let transaction = new SafeInstallOperation();
 
     let moveOldAddon = aId => {
-      let file = getFile(aId, this._directory);
+      let file = getFile(aId, this.dir);
       if (file.exists())
         transaction.moveUnder(file, trashDir);
 
-      file = getFile(`${aId}.xpi`, this._directory);
+      file = getFile(`${aId}.xpi`, this.dir);
       if (file.exists()) {
         flushJarCache(file);
         transaction.moveUnder(file, trashDir);
       }
     };
 
     // If any of these operations fails the finally block will clean up the
     // temporary directory
@@ -2938,152 +2908,149 @@ class MutableDirectoryInstallLocation ex
             }
 
             transaction.moveTo(oldDataDir, newDataDir);
           }
         }
       }
 
       if (action == "copy") {
-        transaction.copy(source, this._directory);
+        transaction.copy(source, this.dir);
       } else if (action == "move") {
         flushJarCache(source);
-        transaction.moveUnder(source, this._directory);
+        transaction.moveUnder(source, this.dir);
       }
       // Do nothing for the proxy file as we sideload an addon permanently
     } finally {
       // It isn't ideal if this cleanup fails but it isn't worth rolling back
       // the install because of it.
       try {
         recursiveRemove(trashDir);
       } catch (e) {
-        logger.warn("Failed to remove trash directory when installing " + id, e);
+        logger.warn(`Failed to remove trash directory when installing ${id}`, e);
       }
     }
 
-    let newFile = this._directory.clone();
+    let newFile = this.dir.clone();
 
     if (action == "proxy") {
       // When permanently installing sideloaded addon, we just put a proxy file
       // referring to the addon sources
       newFile.append(id);
 
       writeStringToFile(newFile, source.path);
     } else {
       newFile.append(source.leafName);
     }
 
     try {
       newFile.lastModifiedTime = Date.now();
     } catch (e) {
-      logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
-    }
-    this._IDToFileMap[id] = newFile;
-
-    if (existingAddonID && existingAddonID != id &&
-        existingAddonID in this._IDToFileMap) {
-      delete this._IDToFileMap[existingAddonID];
+      logger.warn(`failed to set lastModifiedTime on ${newFile.path}`, e);
     }
 
     return newFile;
   }
 
   /**
    * Uninstalls an add-on from this location.
    *
    * @param {string} aId
    *        The ID of the add-on to uninstall
    * @throws if the ID does not match any of the add-ons installed
    */
   uninstallAddon(aId) {
-    let file = this._IDToFileMap[aId];
-    if (!file) {
-      logger.warn("Attempted to remove " + aId + " from " +
-           this._name + " but it was already gone");
-      return;
-    }
-
-    file = getFile(aId, this._directory);
+    let file = getFile(aId, this.dir);
     if (!file.exists())
       file.leafName += ".xpi";
 
     if (!file.exists()) {
-      logger.warn("Attempted to remove " + aId + " from " +
-           this._name + " but it was already gone");
-
-      delete this._IDToFileMap[aId];
+      logger.warn(`Attempted to remove ${aId} from ${this.name} but it was already gone`);
+      this.location.delete(aId);
       return;
     }
 
     let trashDir = this.getTrashDir();
 
     if (file.leafName != aId) {
-      logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
+      logger.debug(`uninstallAddon: flushing jar cache ${file.path} for addon ${aId}`);
       flushJarCache(file);
     }
 
     let transaction = new SafeInstallOperation();
 
     try {
       transaction.moveUnder(file, trashDir);
     } finally {
       // It isn't ideal if this cleanup fails, but it is probably better than
       // rolling back the uninstall at this point
       try {
         recursiveRemove(trashDir);
       } catch (e) {
-        logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
+        logger.warn(`Failed to remove trash directory when uninstalling ${aId}`, e);
       }
     }
 
-    XPIStates.removeAddon(this.name, aId);
-
-    delete this._IDToFileMap[aId];
+    this.location.removeAddon(aId);
   }
 }
 
-class SystemAddonInstallLocation extends MutableDirectoryInstallLocation {
+class SystemAddonInstaller extends DirectoryInstaller {
+  constructor(location) {
+    super(location);
+
+    this._baseDir = location._baseDir;
+    this._nextDir = null;
+  }
+
+  get _addonSet() {
+    return this.location._addonSet;
+  }
+  set _addonSet(val) {
+    this.location._addonSet = val;
+  }
+
   /**
    * Saves the current set of system add-ons
    *
    * @param {Object} aAddonSet - object containing schema, directory and set
    *                 of system add-on IDs and versions.
    */
   static _saveAddonSet(aAddonSet) {
     Services.prefs.setStringPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
   }
 
   static _loadAddonSet() {
-    return XPIInternal.SystemAddonInstallLocation._loadAddonSet();
+    return XPIInternal.SystemAddonLocation._loadAddonSet();
   }
 
   /**
    * Gets the staging directory to put add-ons that are pending install and
    * uninstall into.
    *
    * @returns {nsIFile}
    *        Staging directory for system add-on upgrades.
    */
   getStagingDir() {
-    this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+    this._addonSet = SystemAddonInstaller._loadAddonSet();
     let dir = null;
     if (this._addonSet.directory) {
-      this._directory = getFile(this._addonSet.directory, this._baseDir);
-      dir = getFile(DIR_STAGE, this._directory);
+      this.dir = getFile(this._addonSet.directory, this._baseDir);
+      dir = getFile(DIR_STAGE, this.dir);
     } else {
-      logger.info("SystemAddonInstallLocation directory is missing");
+      logger.info("SystemAddonInstaller directory is missing");
     }
 
     return dir;
   }
 
   requestStagingDir() {
-    this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+    this._addonSet = SystemAddonInstaller._loadAddonSet();
     if (this._addonSet.directory) {
-      this._directory = getFile(this._addonSet.directory, this._baseDir);
+      this.dir = getFile(this._addonSet.directory, this._baseDir);
     }
     return super.requestStagingDir();
   }
 
   isValidAddon(aAddon) {
     if (aAddon.appDisabled) {
       logger.warn(`System add-on ${aAddon.id} isn't compatible with the application.`);
       return false;
@@ -3125,17 +3092,17 @@ class SystemAddonInstallLocation extends
    */
   async resetAddonSet() {
     logger.info("Removing all system add-on upgrades.");
 
     // remove everything from the pref first, if uninstall
     // fails then at least they will not be re-activated on
     // next restart.
     this._addonSet = { schema: 1, addons: {} };
-    SystemAddonInstallLocation._saveAddonSet(this._addonSet);
+    SystemAddonInstaller._saveAddonSet(this._addonSet);
 
     // If this is running at app startup, the pref being cleared
     // will cause later stages of startup to notice that the
     // old updates are now gone.
     //
     // Updates will only be explicitly uninstalled if they are
     // removed restartlessly, for instance if they are no longer
     // part of the latest update set.
@@ -3171,17 +3138,17 @@ class SystemAddonInstallLocation extends
     try {
       for (;;) {
         let {value: entry, done} = await iterator.next();
         if (done) {
           break;
         }
 
         // Skip the directory currently in use
-        if (this._directory && this._directory.path == entry.path) {
+        if (this.dir && this.dir.path == entry.path) {
           continue;
         }
 
         // Skip the next directory
         if (this._nextDir && this._nextDir.path == entry.path) {
           continue;
         }
 
@@ -3209,17 +3176,17 @@ class SystemAddonInstallLocation extends
    * add-on set in prefs.
    *
    * @param {Array} aAddons - An array of addons to install.
    */
   async installAddonSet(aAddons) {
     // Make sure the base dir exists
     await OS.File.makeDir(this._baseDir.path, { ignoreExisting: true });
 
-    let addonSet = SystemAddonInstallLocation._loadAddonSet();
+    let addonSet = SystemAddonInstaller._loadAddonSet();
 
     // Remove any add-ons that are no longer part of the set.
     for (let addonID of Object.keys(addonSet.addons)) {
       if (!aAddons.includes(addonID)) {
         AddonManager.getAddonByID(addonID).then(a => a.uninstall());
       }
     }
 
@@ -3233,72 +3200,71 @@ class SystemAddonInstallLocation extends
         break;
       } catch (e) {
         logger.debug("Could not create new system add-on updates dir, retrying", e);
       }
     }
 
     // Record the new upgrade directory.
     let state = { schema: 1, directory: newDir.leafName, addons: {} };
-    SystemAddonInstallLocation._saveAddonSet(state);
+    SystemAddonInstaller._saveAddonSet(state);
 
     this._nextDir = newDir;
-    let location = this;
 
     let installs = [];
     for (let addon of aAddons) {
-      let install = await createLocalInstall(addon._sourceBundle, location);
+      let install = await createLocalInstall(addon._sourceBundle, this.location);
       installs.push(install);
     }
 
     async function installAddon(install) {
       // Make the new install own its temporary file.
       install.ownsTempFile = true;
       install.install();
     }
 
     async function postponeAddon(install) {
       let resumeFn;
       if (AddonManagerPrivate.hasUpgradeListener(install.addon.id)) {
         logger.info(`system add-on ${install.addon.id} has an upgrade listener, postponing upgrade set until restart`);
         resumeFn = () => {
           logger.info(`${install.addon.id} has resumed a previously postponed addon set`);
-          install.installLocation.resumeAddonSet(installs);
+          install.location.installer.resumeAddonSet(installs);
         };
       }
       await install.postpone(resumeFn);
     }
 
     let previousState;
 
     try {
       // All add-ons in position, create the new state and store it in prefs
       state = { schema: 1, directory: newDir.leafName, addons: {} };
       for (let addon of aAddons) {
         state.addons[addon.id] = {
           version: addon.version
         };
       }
 
-      previousState = SystemAddonInstallLocation._loadAddonSet();
-      SystemAddonInstallLocation._saveAddonSet(state);
+      previousState = SystemAddonInstaller._loadAddonSet();
+      SystemAddonInstaller._saveAddonSet(state);
 
       let blockers = aAddons.filter(
         addon => AddonManagerPrivate.hasUpgradeListener(addon.id)
       );
 
       if (blockers.length > 0) {
         await waitForAllPromises(installs.map(postponeAddon));
       } else {
         await waitForAllPromises(installs.map(installAddon));
       }
     } catch (e) {
       // Roll back to previous upgrade set (if present) on restart.
       if (previousState) {
-        SystemAddonInstallLocation._saveAddonSet(previousState);
+        SystemAddonInstaller._saveAddonSet(previousState);
       }
       // Otherwise, roll back to built-in set on restart.
       // TODO try to do these restartlessly
       this.resetAddonSet();
 
       try {
         await OS.File.removeDir(newDir.path, { ignorePermissions: true });
       } catch (e) {
@@ -3312,17 +3278,17 @@ class SystemAddonInstallLocation extends
   * Resumes upgrade of a previously-delayed add-on set.
   *
   * @param {AddonInstall[]} installs
   *        The set of installs to resume.
   */
   async resumeAddonSet(installs) {
     async function resumeAddon(install) {
       install.state = AddonManager.STATE_DOWNLOADED;
-      install.installLocation.releaseStagingDir();
+      install.location.installer.releaseStagingDir();
       install.install();
     }
 
     let blockers = installs.filter(
       install => AddonManagerPrivate.hasUpgradeListener(install.addon.id)
     );
 
     if (blockers.length > 1) {
@@ -3336,17 +3302,17 @@ class SystemAddonInstallLocation extends
    * Returns a directory that is normally on the same filesystem as the rest of
    * the install location and can be used for temporarily storing files during
    * safe move operations. Calling this method will delete the existing trash
    * directory and its contents.
    *
    * @returns {nsIFile}
    */
   getTrashDir() {
-    let trashDir = getFile(DIR_TRASH, this._directory);
+    let trashDir = getFile(DIR_TRASH, this.dir);
     let trashDirExists = trashDir.exists();
     try {
       if (trashDirExists)
         recursiveRemove(trashDir);
       trashDirExists = false;
     } catch (e) {
       logger.warn("Failed to remove trash directory", e);
     }
@@ -3370,35 +3336,34 @@ class SystemAddonInstallLocation extends
     let trashDir = this.getTrashDir();
     let transaction = new SafeInstallOperation();
 
     // If any of these operations fails the finally block will clean up the
     // temporary directory
     try {
       flushJarCache(source);
 
-      transaction.moveUnder(source, this._directory);
+      transaction.moveUnder(source, this.dir);
     } finally {
       // It isn't ideal if this cleanup fails but it isn't worth rolling back
       // the install because of it.
       try {
         recursiveRemove(trashDir);
       } catch (e) {
-        logger.warn("Failed to remove trash directory when installing " + id, e);
+        logger.warn(`Failed to remove trash directory when installing ${id}`, e);
       }
     }
 
-    let newFile = getFile(source.leafName, this._directory);
+    let newFile = getFile(source.leafName, this.dir);
 
     try {
       newFile.lastModifiedTime = Date.now();
     } catch (e) {
       logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
     }
-    this._IDToFileMap[id] = newFile;
 
     return newFile;
   }
 
   // old system add-on upgrade dirs get automatically removed
   uninstallAddon(aAddon) {}
 }
 
@@ -3413,82 +3378,78 @@ var XPIInstall = {
   recursiveRemove,
   syncLoadManifestFromFile,
 
   /**
    * @param {string} id
    *        The expected ID of the add-on.
    * @param {nsIFile} file
    *        The XPI file to install the add-on from.
-   * @param {InstallLocation} location
+   * @param {XPIStateLocation} location
    *        The install location to install the add-on to.
    * @returns {AddonInternal}
    *        The installed Addon object, upon success.
    */
   async installDistributionAddon(id, file, location) {
     let addon = await loadManifestFromFile(file, location);
 
     if (addon.id != id) {
       throw new Error(`File file ${file.path} contains an add-on with an incorrect ID`);
     }
 
-    let existingEntry = null;
-    try {
-      existingEntry = location.getLocationForID(id);
-    } catch (e) {
-    }
-
-    if (existingEntry) {
+    let state = location.get(id);
+
+    if (state) {
       try {
-        let existingAddon = await loadManifestFromFile(existingEntry, location);
+        let existingAddon = await loadManifestFromFile(state.file, location);
 
         if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
           return null;
       } catch (e) {
         // Bad add-on in the profile so just proceed and install over the top
         logger.warn("Profile contains an add-on with a bad or missing install " +
-                    `manifest at ${existingEntry.path}, overwriting`, e);
+                    `manifest at ${state.path}, overwriting`, e);
       }
     } else if (Services.prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
       return null;
     }
 
     // Install the add-on
-    addon._sourceBundle = location.installAddon({ id, source: file, action: "copy" });
+    addon._sourceBundle = location.installer.installAddon({ id, source: file, action: "copy" });
     if (Services.prefs.getBoolPref(PREF_DISTRO_ADDONS_PERMS, false)) {
       addon.userDisabled = true;
       if (!XPIProvider.newDistroAddons) {
         XPIProvider.newDistroAddons = new Set();
       }
       XPIProvider.newDistroAddons.add(id);
     }
 
     XPIStates.addAddon(addon);
-    logger.debug("Installed distribution add-on " + id);
+    logger.debug(`Installed distribution add-on ${id}`);
 
     Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true);
 
     return addon;
   },
 
   /**
    * Completes the install of an add-on which was staged during the last
    * session.
    *
    * @param {string} id
    *        The expected ID of the add-on.
    * @param {object} metadata
    *        The parsed metadata for the staged install.
-   * @param {InstallLocation} location
+   * @param {XPIStateLocation} location
    *        The install location to install the add-on to.
    * @returns {AddonInternal}
    *        The installed Addon object, upon success.
    */
   async installStagedAddon(id, metadata, location) {
-    let source = getFile(`${id}.xpi`, location.getStagingDir());
+    let source = getFile(`${id}.xpi`, location.installer.getStagingDir());
 
     // Check that the directory's name is a valid ID.
     if (!gIDTest.test(id) || !source.exists() || !source.isFile()) {
       throw new Error(`Ignoring invalid staging directory entry: ${id}`);
     }
 
     let addon = await loadManifestFromFile(source, location);
 
@@ -3511,56 +3472,58 @@ var XPIInstall = {
           XPIInternal.get(existingAddon).uninstall(reason, {newVersion});
         }
       } catch (e) {
         Cu.reportError(e);
       }
     }
 
     try {
-      addon._sourceBundle = location.installAddon({
+      addon._sourceBundle = location.installer.installAddon({
         id, source, existingAddonID: id,
       });
       XPIStates.addAddon(addon);
     } catch (e) {
       if (existingAddon) {
         // Re-install the old add-on
         XPIInternal.get(existingAddon).install();
       }
       throw e;
     }
 
     return addon;
   },
 
   async updateSystemAddons() {
-    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+    let systemAddonLocation = XPIStates.getLocation(KEY_APP_SYSTEM_ADDONS);
     if (!systemAddonLocation)
       return;
 
+    let installer = systemAddonLocation.installer;
+
     // Don't do anything in safe mode
     if (Services.appinfo.inSafeMode)
       return;
 
     // Download the list of system add-ons
     let url = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_UPDATE_URL, null);
     if (!url) {
-      await systemAddonLocation.cleanDirectories();
+      await installer.cleanDirectories();
       return;
     }
 
     url = await UpdateUtils.formatUpdateURL(url);
 
     logger.info(`Starting system add-on update check from ${url}.`);
     let res = await ProductAddonChecker.getProductAddonList(url);
 
     // If there was no list then do nothing.
     if (!res || !res.gmpAddons) {
       logger.info("No system add-ons list was returned.");
-      await systemAddonLocation.cleanDirectories();
+      await installer.cleanDirectories();
       return;
     }
 
     let addonList = new Map(
       res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }]));
 
     let setMatches = (wanted, existing) => {
       if (wanted.size != existing.size)
@@ -3577,27 +3540,27 @@ var XPIInstall = {
 
       return true;
     };
 
     // If this matches the current set in the profile location then do nothing.
     let updatedAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_ADDONS));
     if (setMatches(addonList, updatedAddons)) {
       logger.info("Retaining existing updated system add-ons.");
-      await systemAddonLocation.cleanDirectories();
+      await installer.cleanDirectories();
       return;
     }
 
     // If this matches the current set in the default location then reset the
     // updated set.
     let defaultAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS));
     if (setMatches(addonList, defaultAddons)) {
       logger.info("Resetting system add-ons.");
-      systemAddonLocation.resetAddonSet();
-      await systemAddonLocation.cleanDirectories();
+      installer.resetAddonSet();
+      await installer.cleanDirectories();
       return;
     }
 
     // Download all the add-ons
     async function downloadAddon(item) {
       try {
         let sourceAddon = updatedAddons.get(item.spec.id);
         if (sourceAddon && sourceAddon.version == item.spec.version) {
@@ -3636,30 +3599,30 @@ var XPIInstall = {
         return false;
       }
 
       if (item.spec.version != item.addon.version) {
         logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`);
         return false;
       }
 
-      if (!systemAddonLocation.isValidAddon(item.addon))
+      if (!installer.isValidAddon(item.addon))
         return false;
 
       return true;
     };
 
     if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
       throw new Error("Rejecting updated system add-on set that either could not " +
                       "be downloaded or contained unusable add-ons.");
     }
 
     // Install into the install location
     logger.info("Installing new system add-on set");
-    await systemAddonLocation.installAddonSet(Array.from(addonList.values())
+    await installer.installAddonSet(Array.from(addonList.values())
       .map(a => a.addon));
   },
 
   /**
    * Called to test whether installing XPI add-ons is enabled.
    *
    * @returns {boolean}
    *        True if installing is enabled.
@@ -3747,17 +3710,17 @@ var XPIInstall = {
    *        Icon URLs for the install
    * @param {string} [aVersion]
    *        A version for the install
    * @param {XULElement?} [aBrowser]
    *        The browser performing the install
    * @returns {AddonInstall}
    */
   async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser) {
-    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+    let location = XPIStates.getLocation(KEY_APP_PROFILE);
     let url = Services.io.newURI(aUrl);
 
     let options = {
       hash: aHash,
       browser: aBrowser,
       name: aName,
       icons: aIcons,
       version: aVersion,
@@ -3820,17 +3783,17 @@ var XPIInstall = {
   async installTemporaryAddon(aFile) {
     let installLocation = XPIInternal.TemporaryInstallLocation;
 
     if (aFile.exists() && aFile.isFile()) {
       flushJarCache(aFile);
     }
     let addon = await loadManifestFromFile(aFile, installLocation);
 
-    installLocation.installAddon({ id: addon.id, source: aFile });
+    installLocation.installer.installAddon({ id: addon.id, source: aFile });
 
     if (addon.appDisabled) {
       let message = `Add-on ${addon.id} is not compatible with application version.`;
 
       let app = addon.matchingTargetApplication;
       if (app) {
         if (app.minVersion) {
           message += ` add-on minVersion: ${app.minVersion}.`;
@@ -3904,50 +3867,50 @@ var XPIInstall = {
    *        Force this addon into the pending uninstall state (used
    *        e.g. while the add-on manager is open and offering an
    *        "undo" button)
    * @throws if the addon cannot be uninstalled because it is in an install
    *         location that does not allow it
    */
   async uninstallAddon(aAddon, aForcePending) {
     if (!(aAddon.inDatabase))
-      throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
-
-    if (aAddon._installLocation.locked)
-      throw new Error("Cannot uninstall addon " + aAddon.id
-          + " from locked install location " + aAddon._installLocation.name);
+      throw new Error(`Cannot uninstall addon ${aAddon.id} because it is not installed`);
+
+    if (aAddon.location.locked)
+      throw new Error(`Cannot uninstall addon ${aAddon.id} ` +
+                      `from locked install location ${aAddon.location.name}`);
 
     if (aForcePending && aAddon.pendingUninstall)
       throw new Error("Add-on is already marked to be uninstalled");
 
     aAddon._hasResourceCache.clear();
 
     if (aAddon._updateCheck) {
-      logger.debug("Cancel in-progress update check for " + aAddon.id);
+      logger.debug(`Cancel in-progress update check for ${aAddon.id}`);
       aAddon._updateCheck.cancel();
     }
 
     let wasPending = aAddon.pendingUninstall;
 
     if (aForcePending) {
       // We create an empty directory in the staging directory to indicate
       // that an uninstall is necessary on next startup. Temporary add-ons are
       // automatically uninstalled on shutdown anyway so there is no need to
       // do this for them.
-      if (aAddon._installLocation.name != KEY_APP_TEMPORARY) {
-        let stage = getFile(aAddon.id, aAddon._installLocation.getStagingDir());
+      if (!aAddon.location.isTemporary) {
+        let stage = getFile(aAddon.id, aAddon.location.installer.getStagingDir());
         if (!stage.exists())
           stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
       }
 
       XPIDatabase.setAddonProperties(aAddon, {
         pendingUninstall: true
       });
       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
-      let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+      let xpiState = aAddon.location.get(aAddon.id);
       if (xpiState) {
         xpiState.enabled = false;
         XPIStates.save();
       } else {
         logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
       }
     }
 
@@ -3958,32 +3921,32 @@ var XPIInstall = {
     let wrapper = aAddon.wrapper;
 
     // If the add-on wasn't already pending uninstall then notify listeners.
     if (!wasPending) {
       AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
                                              !!aForcePending);
     }
 
-    let existingAddon = XPIStates.findAddon(aAddon.id, loc =>
-      loc.name != aAddon._installLocation.name);
+    let existingAddon = XPIStates.findAddon(aAddon.id,
+                                            loc => loc != aAddon.location);
 
     let bootstrap = XPIInternal.BootstrapScope.get(aAddon);
     if (!aForcePending) {
       let existing;
       if (existingAddon) {
         existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
       }
 
       let uninstall = () => {
         XPIStates.disableAddon(aAddon.id);
 
-        aAddon._installLocation.uninstallAddon(aAddon.id);
+        aAddon.location.installer.uninstallAddon(aAddon.id);
         XPIDatabase.removeAddonMetadata(aAddon);
-        XPIStates.removeAddon(aAddon.location, aAddon.id);
+        aAddon.location.removeAddon(aAddon.id);
         AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
         if (existing) {
           XPIDatabase.makeAddonVisible(existing);
           AddonManagerPrivate.callAddonListeners("onInstalling", existing.wrapper, false);
 
           if (!existing.disabled) {
             XPIDatabase.updateAddonActive(existing, true);
@@ -3991,17 +3954,17 @@ var XPIInstall = {
         }
       };
 
       if (existing) {
         bootstrap.update(existing, !existing.disabled, uninstall);
 
         AddonManagerPrivate.callAddonListeners("onInstalled", existing.wrapper);
       } else {
-        XPIStates.removeAddon(aAddon.location, aAddon.id);
+        aAddon.location.removeAddon(aAddon.id);
         bootstrap.uninstall();
         uninstall();
       }
     } else if (aAddon.active) {
       XPIStates.disableAddon(aAddon.id);
       bootstrap.shutdown(BOOTSTRAP_REASONS.ADDON_UNINSTALL);
       XPIDatabase.updateAddonActive(aAddon, false);
     }
@@ -4018,27 +3981,27 @@ var XPIInstall = {
    *        The DBAddonInternal to cancel uninstall for
    */
   cancelUninstallAddon(aAddon) {
     if (!(aAddon.inDatabase))
       throw new Error("Can only cancel uninstall for installed addons.");
     if (!aAddon.pendingUninstall)
       throw new Error("Add-on is not marked to be uninstalled");
 
-    if (aAddon._installLocation.name != KEY_APP_TEMPORARY)
-      aAddon._installLocation.cleanStagingDir([aAddon.id]);
+    if (!aAddon.location.isTemporary)
+      aAddon.location.installer.cleanStagingDir([aAddon.id]);
 
     XPIDatabase.setAddonProperties(aAddon, {
       pendingUninstall: false
     });
 
     if (!aAddon.visible)
       return;
 
-    XPIStates.getAddon(aAddon.location, aAddon.id).syncWithDB(aAddon);
+    aAddon.location.get(aAddon.id).syncWithDB(aAddon);
     XPIStates.save();
 
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
     // TODO hide hidden add-ons (bug 557710)
     let wrapper = aAddon.wrapper;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
 
@@ -4047,11 +4010,11 @@ var XPIInstall = {
       XPIDatabase.updateAddonActive(aAddon, true);
     }
 
     // Notify any other providers that this theme is now enabled again.
     if (isTheme(aAddon.type) && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
   },
 
-  MutableDirectoryInstallLocation,
-  SystemAddonInstallLocation,
+  DirectoryInstaller,
+  SystemAddonInstaller,
 };
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -64,19 +64,16 @@ const PREF_EM_STARTUP_SCAN_SCOPES     = 
 const PREF_XPI_SIGNATURES_REQUIRED    = "xpinstall.signatures.required";
 const PREF_LANGPACK_SIGNATURES        = "extensions.langpacks.signatures.required";
 const PREF_XPI_PERMISSIONS_BRANCH     = "xpinstall.";
 const PREF_INSTALL_DISTRO_ADDONS      = "extensions.installDistroAddons";
 const PREF_BRANCH_INSTALLED_ADDON     = "extensions.installedDistroAddon.";
 const PREF_SYSTEM_ADDON_SET           = "extensions.systemAddonSet";
 const PREF_ALLOW_LEGACY               = "extensions.legacy.enabled";
 
-const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
-const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
-
 const PREF_EM_LAST_APP_BUILD_ID       = "extensions.lastAppBuildId";
 
 // Specify a list of valid built-in add-ons to load.
 const BUILT_IN_ADDONS_URI             = "chrome://browser/content/built_in_addons.json";
 
 const OBSOLETE_PREFERENCES = [
   "extensions.bootstrappedAddons",
   "extensions.enabledAddons",
@@ -362,25 +359,22 @@ function isTheme(type) {
  */
 function canRunInSafeMode(aAddon) {
   // Even though the updated system add-ons aren't generally run in safe mode we
   // include them here so their uninstall functions get called when switching
   // back to the default set.
 
   // TODO product should make the call about temporary add-ons running
   // in safe mode. assuming for now that they are.
-  let location = aAddon._installLocation || null;
+  let location = aAddon.location || null;
   if (!location) {
     return false;
   }
 
-  if (location.name == KEY_APP_TEMPORARY)
-    return true;
-
-  return location.isSystem;
+  return location.isTemporary || location.isSystem;
 }
 
 /**
  * Converts an internal add-on type to the type presented through the API.
  *
  * @param {string} aType
  *        The internal add-on type
  * @returns {string}
@@ -468,45 +462,35 @@ function getURIForResourceInFile(aFile, 
  */
 function buildJarURI(aJarfile, aPath) {
   let uri = Services.io.newFileURI(aJarfile);
   uri = "jar:" + uri.spec + "!/" + aPath;
   return Services.io.newURI(uri);
 }
 
 /**
- * Gets a snapshot of directory entries.
+ * Iterates over the entries in a given directory.
+ *
+ * Fails silently if the given directory does not exist.
  *
- * @param {nsIURI} aDir
- *        Directory to look at
- * @param {boolean} aSortEntries
- *        True to sort entries by filename
- * @returns {Array<nsIFile>}
- *        An array of nsIFile, or an empty array if aDir is not a readable directory
+ * @param {nsIFile} aDir
+ *        Directory to iterate.
  */
-function getDirectoryEntries(aDir, aSortEntries) {
+function* iterDirectory(aDir) {
   let dirEnum;
   try {
-    dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-    let entries = [];
-    while (dirEnum.hasMoreElements())
-      entries.push(dirEnum.nextFile);
-
-    if (aSortEntries) {
-      entries.sort(function(a, b) {
-        return a.path > b.path ? -1 : 1;
-      });
+    dirEnum = aDir.directoryEntries;
+    let file;
+    while ((file = dirEnum.nextFile)) {
+      yield file;
     }
-
-    return entries;
   } catch (e) {
     if (aDir.exists()) {
-      logger.warn("Can't iterate directory " + aDir.path, e);
+      logger.warn(`Can't iterate directory ${aDir.path}`, e);
     }
-    return [];
   } finally {
     if (dirEnum) {
       dirEnum.close();
     }
   }
 }
 
 /**
@@ -741,39 +725,73 @@ class XPIState {
   }
 }
 
 /**
  * Manages the state data for add-ons in a given install location.
  *
  * @param {string} name
  *        The name of the install location (e.g., "app-profile").
- * @param {string?} path
+ * @param {string | nsIFile | null} path
  *        The on-disk path of the install location. May be null for some
  *        locations which do not map to a specific on-disk path.
- * @param {object} [saved = {}]
+ * @param {integer} scope
+ *        The scope of add-ons installed in this location.
+ * @param {object} [saved]
  *        The persisted JSON state data to restore.
  */
 class XPIStateLocation extends Map {
-  constructor(name, path, saved = {}) {
+  constructor(name, path, scope, saved) {
     super();
 
     this.name = name;
-    this.path = path || saved.path || null;
+    this.scope = scope;
+    if (path instanceof Ci.nsIFile) {
+      this.dir = path;
+      this.path = path.path;
+    } else {
+      this.path = path;
+      this.dir = this.path && new nsIFile(this.path);
+    }
+    this.staged = {};
+    this.changed = false;
+
+    if (saved) {
+      this.restore(saved);
+    }
+
+    this._installler = undefined;
+  }
+
+  get installer() {
+    if (this._installer === undefined) {
+      this._installer = this.makeInstaller();
+    }
+    return this._installer;
+  }
+
+  makeInstaller() {
+    return null;
+  }
+
+  restore(saved) {
+    if (!this.path && saved.path) {
+      this.path = saved.path;
+      this.dir = new nsIFile(this.path);
+    }
     this.staged = saved.staged || {};
     this.changed = saved.changed || false;
-    this.dir = this.path && new nsIFile(this.path);
 
     for (let [id, data] of Object.entries(saved.addons || {})) {
       let xpiState = this._addState(id, data);
 
       // Make a note that this state was restored from saved data. But
       // only if this location hasn't moved since the last startup,
       // since that causes problems for new system add-on bundles.
-      if (!path || path == saved.path) {
+      if (!this.path || this.path == saved.path) {
         xpiState.wasRestored = true;
       }
     }
   }
 
   /**
    * Returns a JSON-compatible representation of this location's state
    * data, to be saved to addonStartup.json.
@@ -824,16 +842,29 @@ class XPIStateLocation extends Map {
 
     let xpiState = this._addState(addon.id, {file: addon._sourceBundle});
     xpiState.syncWithDB(addon, true);
 
     XPIProvider.setTelemetry(addon.id, "location", this.name);
   }
 
   /**
+   * Remove the XPIState for an add-on and save the new state.
+   *
+   * @param {string} aId
+   *        The ID of the add-on.
+   */
+  removeAddon(aId) {
+    if (this.has(aId)) {
+      this.delete(aId);
+      XPIStates.save();
+    }
+  }
+
+  /**
    * Adds stub state data for the local file to the DB.
    *
    * @param {string} addonId
    *        The ID of the add-on represented by the given file.
    * @param {nsIFile} file
    *        The local file or directory containing the add-on.
    * @returns {XPIState}
    */
@@ -890,41 +921,475 @@ class XPIStateLocation extends Map {
    *        The add-on's data from the xpiState preference.
    * @param {object} [bootstrapped]
    *        The add-on's data from the bootstrappedAddons preference, if
    *        applicable.
    */
   migrateAddon(id, state, bootstrapped) {
     this.set(id, XPIState.migrate(this, id, state, bootstrapped));
   }
+
+  /**
+   * Returns true if the given addon was installed in this location by a text
+   * file pointing to its real path.
+   *
+   * @param {string} aId
+   *        The ID of the addon
+   * @returns {boolean}
+   */
+  isLinkedAddon(aId) {
+    if (!this.dir) {
+      return true;
+    }
+    return this.has(aId) && !this.dir.contains(this.get(aId).file);
+  }
+
+  get isTemporary() {
+    return false;
+  }
+
+  get isSystem() {
+    return false;
+  }
+}
+
+class TemporaryLocation extends XPIStateLocation {
+  /**
+   * @param {string} name
+   *        The string identifier for the install location.
+   */
+  constructor(name) {
+    super(name, null, null);
+    this.locked = false;
+  }
+
+  makeInstaller() {
+    // Installs are a no-op. We only register that add-ons exist, and
+    // run them from their current location.
+    return {
+      installAddon() {},
+      uninstallAddon() {},
+    };
+  }
+
+  toJSON() {
+    return {};
+  }
+
+  readAddons() {
+    return new Map();
+  }
+
+  get isTemporary() {
+    return true;
+  }
+}
+
+var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
+
+/**
+ * An object which identifies a directory install location for add-ons. The
+ * location consists of a directory which contains the add-ons installed in the
+ * location.
+ *
+ */
+class DirectoryLocation extends XPIStateLocation {
+  /**
+   * Each add-on installed in the location is either a directory containing the
+   * add-on's files or a text file containing an absolute path to the directory
+   * containing the add-ons files. The directory or text file must have the same
+   * name as the add-on's ID.
+   *
+   * @param {string} name
+   *        The string identifier for the install location.
+   * @param {nsIFile} dir
+   *        The directory for the install location.
+   * @param {integer} scope
+   *        The scope of add-ons installed in this location.
+   * @param {boolean} [locked = true]
+   *        If false, the location accepts new add-on installs.
+   */
+  constructor(name, dir, scope, locked = true) {
+    super(name, dir, scope);
+    this.locked = locked;
+    this.initialized = false;
+  }
+
+  makeInstaller() {
+    if (this.locked) {
+      return null;
+    }
+    return new XPIInstall.DirectoryInstaller(this);
+  }
+
+  /**
+   * Reads a single-line file containing the path to a directory, and
+   * returns an nsIFile pointing to that directory, if successful.
+   *
+   * @param {nsIFile} aFile
+   *        The file containing the directory path
+   * @returns {nsIFile?}
+   *        An nsIFile object representing the linked directory, or null
+   *        on error.
+   */
+  _readLinkFile(aFile) {
+    let linkedDirectory;
+    if (aFile.isSymlink()) {
+      linkedDirectory = aFile.clone();
+      try {
+        linkedDirectory.normalize();
+      } catch (e) {
+        logger.warn(`Symbolic link ${aFile.path} points to a path ` +
+                    `which does not exist`);
+        return null;
+      }
+    } else {
+      let fis = new FileInputStream(aFile, -1, -1, false);
+      let line = {};
+      fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
+      fis.close();
+
+      if (line.value) {
+        linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        try {
+          linkedDirectory.initWithPath(line.value);
+        } catch (e) {
+          linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
+        }
+      }
+    }
+
+    if (linkedDirectory) {
+      if (!linkedDirectory.exists()) {
+        logger.warn(`File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+                    "which does not exist");
+        return null;
+      }
+
+      if (!linkedDirectory.isDirectory()) {
+        logger.warn(`File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+                    "which is not a directory");
+        return null;
+      }
+
+      return linkedDirectory;
+    }
+
+    logger.warn(`File pointer ${aFile.path} does not contain a path`);
+    return null;
+  }
+
+  /**
+   * Finds all the add-ons installed in this location.
+   *
+   * @returns {Map<AddonID, nsIFile>}
+   *        A map of add-ons present in this location.
+   */
+  readAddons() {
+    let addons = new Map();
+
+    if (!this.dir) {
+      return addons;
+    }
+    this.initialized = true;
+
+    // Use a snapshot of the directory contents to avoid possible issues with
+    // iterating over a directory while removing files from it (the YAFFS2
+    // embedded filesystem has this issue, see bug 772238).
+    for (let entry of Array.from(iterDirectory(this.dir))) {
+      let id = entry.leafName;
+      if (id == DIR_STAGE || id == DIR_TRASH)
+        continue;
+
+      let isFile = id.toLowerCase().endsWith(".xpi");
+      if (isFile) {
+        id = id.substring(0, id.length - 4);
+      }
+
+      if (!gIDTest.test(id)) {
+        logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
+                     entry.path);
+        continue;
+      }
+
+      if (!isFile && (entry.isFile() || entry.isSymlink())) {
+        let newEntry = this._readLinkFile(entry);
+        if (!newEntry) {
+          logger.debug(`Deleting stale pointer file ${entry.path}`);
+          try {
+            entry.remove(true);
+          } catch (e) {
+            logger.warn(`Failed to remove stale pointer file ${entry.path}`, e);
+            // Failing to remove the stale pointer file is ignorable
+          }
+          continue;
+        }
+
+        entry = newEntry;
+      }
+
+
+      addons.set(id, entry);
+    }
+    return addons;
+  }
+}
+
+/**
+ * An object which identifies a built-in install location for add-ons, such
+ * as default system add-ons.
+ *
+ * This location should point either to a XPI, or a directory in a local build.
+ */
+class BuiltInLocation extends DirectoryLocation {
+  /**
+   * Read the manifest of allowed add-ons and build a mapping between ID and URI
+   * for each.
+   *
+   * @returns {Map<AddonID, nsIFile>}
+   *        A map of add-ons present in this location.
+   */
+  readAddons() {
+    let addons = new Map();
+
+    let manifest;
+    try {
+      let url = Services.io.newURI(BUILT_IN_ADDONS_URI);
+      let data = Cu.readUTF8URI(url);
+      manifest = JSON.parse(data);
+    } catch (e) {
+      logger.warn("List of valid built-in add-ons could not be parsed.", e);
+      return addons;
+    }
+
+    if (!("system" in manifest)) {
+      logger.warn("No list of valid system add-ons found.");
+      return addons;
+    }
+
+    for (let id of manifest.system) {
+      let file = this.dir.clone();
+      file.append(`${id}.xpi`);
+
+      // Only attempt to load unpacked directory if unofficial build.
+      if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
+        file = this.dir.clone();
+        file.append(`${id}`);
+      }
+
+      addons.set(id, file);
+    }
+
+    return addons;
+  }
+
+  get isSystem() {
+    return true;
+  }
+}
+
+/**
+ * An object which identifies a directory install location for system add-ons
+ * updates.
+ */
+class SystemAddonLocation extends DirectoryLocation {
+  /**
+   * The location consists of a directory which contains the add-ons installed.
+   *
+   * @param {string} name
+   *        The string identifier for the install location.
+   * @param {nsIFile} dir
+   *        The directory for the install location.
+   * @param {integer} scope
+   *        The scope of add-ons installed in this location.
+   * @param {boolean} resetSet
+   *        True to throw away the current add-on set
+   */
+  constructor(name, dir, scope, resetSet) {
+    let addonSet = SystemAddonLocation._loadAddonSet();
+    let directory = null;
+
+    // The system add-on update directory is stored in a pref.
+    // Therefore, this is looked up before calling the
+    // constructor on the superclass.
+    if (addonSet.directory) {
+      directory = getFile(addonSet.directory, dir);
+      logger.info(`SystemAddonLocation scanning directory ${directory.path}`);
+    } else {
+      logger.info("SystemAddonLocation directory is missing");
+    }
+
+    super(name, directory, scope, false);
+
+    this._addonSet = addonSet;
+    this._baseDir = dir;
+
+    if (resetSet) {
+      this.installer.resetAddonSet();
+    }
+  }
+
+  makeInstaller() {
+    if (this.locked) {
+      return null;
+    }
+    return new XPIInstall.SystemAddonInstaller(this);
+  }
+
+  /**
+   * Reads the current set of system add-ons
+   *
+   * @returns {Object}
+   */
+  static _loadAddonSet() {
+    try {
+      let setStr = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_SET, null);
+      if (setStr) {
+        let addonSet = JSON.parse(setStr);
+        if ((typeof addonSet == "object") && addonSet.schema == 1) {
+          return addonSet;
+        }
+      }
+    } catch (e) {
+      logger.error("Malformed system add-on set, resetting.");
+    }
+
+    return { schema: 1, addons: {} };
+  }
+
+  readAddons() {
+    // Updated system add-ons are ignored in safe mode
+    if (Services.appinfo.inSafeMode) {
+      return new Map();
+    }
+
+    let addons = super.readAddons();
+
+    // Strip out any unexpected add-ons from the list
+    for (let id of addons.keys()) {
+      if (!(id in this._addonSet.addons)) {
+        addons.delete(id);
+      }
+    }
+
+    return addons;
+  }
+
+  /**
+   * Tests whether updated system add-ons are expected.
+   *
+   * @returns {boolean}
+   */
+  isActive() {
+    return this.dir != null;
+  }
+
+  get isSystem() {
+    return true;
+  }
+}
+
+/**
+ * An object that identifies a registry install location for add-ons. The location
+ * consists of a registry key which contains string values mapping ID to the
+ * path where an add-on is installed
+ *
+ */
+class WinRegLocation extends XPIStateLocation {
+  /**
+   * @param {string} name
+   *        The string identifier for the install location.
+   * @param {integer} rootKey
+   *        The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+   * @param {integer} scope
+   *        The scope of add-ons installed in this location.
+   */
+  constructor(name, rootKey, scope) {
+    super(name, undefined, scope);
+
+    this.locked = true;
+    this._rootKey = rootKey;
+  }
+
+  /**
+   * Retrieves the path of this Application's data key in the registry.
+   */
+  get _appKeyPath() {
+    let appVendor = Services.appinfo.vendor;
+    let appName = Services.appinfo.name;
+
+    // XXX Thunderbird doesn't specify a vendor string
+    if (appVendor == "" && AppConstants.MOZ_APP_NAME == "thunderbird")
+      appVendor = "Mozilla";
+
+    return `SOFTWARE\\${appVendor}\\${appName}`;
+  }
+
+  /**
+   * Read the registry and build a mapping between ID and path for each
+   * installed add-on.
+   *
+   * @returns {Map<AddonID, nsIFile>}
+   *        A map of add-ons in this location.
+   */
+  readAddons() {
+    let addons = new Map();
+
+    let path = `${this._appKeyPath}\\Extensions`;
+    let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(Ci.nsIWindowsRegKey);
+
+    // Reading the registry may throw an exception, and that's ok.  In error
+    // cases, we just leave ourselves in the empty state.
+    try {
+      key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
+    } catch (e) {
+      return addons;
+    }
+
+    try {
+      let count = key.valueCount;
+      for (let i = 0; i < count; ++i) {
+        let id = key.getValueName(i);
+        let file = new nsIFile(key.readStringValue(id));
+        if (!file.exists()) {
+          logger.warn(`Ignoring missing add-on in ${file.path}`);
+          continue;
+        }
+
+        addons.set(id, file);
+      }
+    } finally {
+      key.close();
+    }
+
+    return addons;
+  }
 }
 
 /**
  * Keeps track of the state of XPI add-ons on the file system.
  */
 var XPIStates = {
-  // Map(location name -> Map(add-on ID -> XPIState))
-  db: null,
+  // Map(location-name -> XPIStateLocation)
+  db: new Map(),
 
   _jsonFile: null,
 
   /**
    * @property {Map<string, XPIState>} sideLoadedAddons
    *        A map of new add-ons detected during install location
    *        directory scans. Keys are add-on IDs, values are XPIState
    *        objects corresponding to those add-ons.
    */
   sideLoadedAddons: new Map(),
 
   get size() {
     let count = 0;
-    if (this.db) {
-      for (let location of this.db.values()) {
-        count += location.size;
-      }
+    for (let location of this.locations()) {
+      count += location.size;
     }
     return count;
   },
 
   /**
    * Migrates state data from the xpiState and bootstrappedAddons
    * preferences and adds it to the DB. Returns a JSON-compatible
    * representation of the current state of the DB.
@@ -1006,118 +1471,112 @@ var XPIStates = {
    *
    * @param {boolean} [ignoreSideloads = true]
    *        If true, ignore changes in scopes where we don't accept
    *        side-loads.
    *
    * @returns {boolean}
    *        True if anything has changed.
    */
-  getInstallState(ignoreSideloads = true) {
-    if (!this.db) {
-      this.db = new Map();
-    }
-
+  scanForChanges(ignoreSideloads = true) {
     let oldState = this.initialStateData || this.loadExtensionState();
     this.initialStateData = oldState;
 
     let changed = false;
     let oldLocations = new Set(Object.keys(oldState));
 
-    for (let location of XPIProvider.installLocations) {
-      oldLocations.delete(location.name);
-
-      // The results of scanning this location.
-      let loc = this.getLocation(location.name, location.path || null,
-                                 oldState[location.name] || undefined);
+    for (let loc of XPIStates.locations()) {
+      oldLocations.delete(loc.name);
+
+      if (oldState[loc.name]) {
+        loc.restore(oldState[loc.name]);
+      }
       changed = changed || loc.changed;
 
       // Don't bother checking scopes where we don't accept side-loads.
-      if (ignoreSideloads && !(location.scope & gStartupScanScopes)) {
+      if (ignoreSideloads && !(loc.scope & gStartupScanScopes)) {
         continue;
       }
 
-      if (location.name == KEY_APP_TEMPORARY) {
+      if (loc.isTemporary) {
         continue;
       }
 
       let knownIds = new Set(loc.keys());
-      for (let [id, file] of location.getAddonLocations(true)) {
+      for (let [id, file] of loc.readAddons()) {
         knownIds.delete(id);
 
         let xpiState = loc.get(id);
         if (!xpiState) {
-          logger.debug("New add-on ${id} in ${location}", {id, location: location.name});
+          logger.debug("New add-on ${id} in ${loc}", {id, loc: loc.name});
 
           changed = true;
           xpiState = loc.addFile(id, file);
-          if (!location.isSystem) {
+          if (!loc.isSystem) {
             this.sideLoadedAddons.set(id, xpiState);
           }
         } else {
           let addonChanged = (xpiState.getModTime(file, id) ||
                               file.path != xpiState.path);
           xpiState.file = file.clone();
 
           if (addonChanged) {
             changed = true;
-            logger.debug("Changed add-on ${id} in ${location}", {id, location: location.name});
+            logger.debug("Changed add-on ${id} in ${loc}", {id, loc: loc.name});
           } else {
-            logger.debug("Existing add-on ${id} in ${location}", {id, location: location.name});
+            logger.debug("Existing add-on ${id} in ${loc}", {id, loc: loc.name});
           }
         }
-        XPIProvider.setTelemetry(id, "location", location.name);
+        XPIProvider.setTelemetry(id, "location", loc.name);
       }
 
       // Anything left behind in oldState was removed from the file system.
       for (let id of knownIds) {
         loc.delete(id);
         changed = true;
       }
     }
 
     // If there's anything left in oldState, an install location that held add-ons
     // was removed from the browser configuration.
     changed = changed || oldLocations.size > 0;
 
-    logger.debug("getInstallState changed: ${rv}, state: ${state}",
+    logger.debug("scanForChanges changed: ${rv}, state: ${state}",
         {rv: changed, state: this.db});
     return changed;
   },
 
+  locations() {
+    return this.db.values();
+  },
+
+  /**
+   * @param {string} name
+   *        The location name.
+   * @param {XPIStateLocation} location
+   *        The location object.
+   */
+  addLocation(name, location) {
+    if (this.db.has(name)) {
+      throw new Error(`Trying to add duplicate location: ${name}`);
+    }
+    this.db.set(name, location);
+  },
+
   /**
    * Get the Map of XPI states for a particular location.
    *
    * @param {string} name
    *        The name of the install location.
-   * @param {string?} [path]
-   *        The expected path of the location, if known.
-   * @param {Object?} [saved]
-   *        The saved data for the location, as read from the
-   *        addonStartup.json file.
    *
    * @returns {XPIStateLocation?}
    *        (id -> XPIState) or null if there are no add-ons in the location.
    */
-  getLocation(name, path, saved) {
-    let location = this.db.get(name);
-
-    if (path && location && location.path != path) {
-      location = null;
-      saved = null;
-    }
-
-    if (!location || (path && location.path != path)) {
-      let loc = XPIProvider.installLocationsByName[name];
-      if (loc) {
-        location = new XPIStateLocation(name, path || loc.path || null, saved);
-        this.db.set(name, location);
-      }
-    }
-    return location;
+  getLocation(name) {
+    return this.db.get(name);
   },
 
   /**
    * Get the XPI state for a specific add-on in a location.
    * If the state is not in our cache, return null.
    *
    * @param {string} aLocation
    *        The name of the location where the add-on is installed.
@@ -1141,49 +1600,48 @@ var XPIStates = {
    *        An optional filter to apply to install locations.  If provided,
    *        addons in locations that do not match the filter are not considered.
    *
    * @returns {XPIState?}
    */
   findAddon(aId, aFilter = location => true) {
     // Fortunately the Map iterator returns in order of insertion, which is
     // also our highest -> lowest priority order.
-    for (let location of this.db.values()) {
+    for (let location of this.locations()) {
       if (!aFilter(location)) {
         continue;
       }
       if (location.has(aId)) {
         return location.get(aId);
       }
     }
     return undefined;
   },
 
   /**
    * Iterates over the list of all enabled add-ons in any location.
    */
   * enabledAddons() {
-    for (let location of this.db.values()) {
+    for (let location of this.locations()) {
       for (let entry of location.values()) {
         if (entry.enabled) {
           yield entry;
         }
       }
     }
   },
 
   /**
    * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
    *
    * @param {DBAddonInternal} aAddon
    *        The add-on to add.
    */
   addAddon(aAddon) {
-    let location = this.getLocation(aAddon._installLocation.name);
-    location.addAddon(aAddon);
+    aAddon.location.addAddon(aAddon);
   },
 
   /**
    * Save the current state of installed add-ons.
    */
   save() {
     if (!this._jsonFile) {
       this._jsonFile = new JSONFile({
@@ -1195,40 +1653,37 @@ var XPIStates = {
     }
 
     this._jsonFile.saveSoon();
   },
 
   toJSON() {
     let data = {};
     for (let [key, loc] of this.db.entries()) {
-      if (key != TemporaryInstallLocation.name && (loc.size || loc.hasStaged)) {
+      if (!loc.isTemporary && (loc.size || loc.hasStaged)) {
         data[key] = loc;
       }
     }
     return data;
   },
 
   /**
    * Remove the XPIState for an add-on and save the new state.
    *
    * @param {string} aLocation
    *        The name of the add-on location.
    * @param {string} aId
    *        The ID of the add-on.
    *
    */
   removeAddon(aLocation, aId) {
-    logger.debug("Removing XPIState for " + aLocation + ":" + aId);
+    logger.debug(`Removing XPIState for ${aLocation}: ${aId}`);
     let location = this.db.get(aLocation);
     if (location) {
-      location.delete(aId);
-      if (location.size == 0) {
-        this.db.delete(aLocation);
-      }
+      location.removeAddon(aId);
       this.save();
     }
   },
 
   /**
    * Disable the XPIState for an add-on.
    *
    * @param {string} aId
@@ -1338,25 +1793,24 @@ class BootstrapScope {
           this._pendingDisable = true;
           for (let addon of XPIProvider.getDependentAddons(this.addon)) {
             if (addon.active)
               XPIDatabase.updateAddonDisabledState(addon);
           }
         }
       }
 
-      let installLocation = addon._installLocation || null;
       let params = {
         id: addon.id,
         version: addon.version,
         installPath: this.file.clone(),
         resourceURI: getURIForResourceInFile(this.file, ""),
         signedState: addon.signedState,
-        temporarilyInstalled: installLocation == TemporaryInstallLocation,
-        builtIn: installLocation instanceof BuiltInInstallLocation,
+        temporarilyInstalled: addon.location.isTemporary,
+        builtIn: addon.location instanceof BuiltInLocation,
       };
 
       if (aMethod == "startup" && addon.startupData) {
         params.startupData = addon.startupData;
       }
 
       Object.assign(params, aExtraParams);
 
@@ -1427,57 +1881,39 @@ class BootstrapScope {
     // But not at app startup, since we'll already have added all of our
     // annotations before starting any loads.
     if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
       XPIProvider.addAddonsToCrashReporter();
     }
 
     logger.debug(`Loading bootstrap scope from ${this.file.path}`);
 
-    let principal = Services.scriptSecurityManager.getSystemPrincipal();
-
-    if (!this.file.exists()) {
-      this.scope =
-        new Cu.Sandbox(principal, { sandboxName: this.file.path,
-                                    addonId: this.addon.id,
-                                    wantGlobalProperties: ["ChromeUtils"],
-                                    metadata: { addonID: this.addon.id } });
-      logger.error(`Attempted to load bootstrap scope from missing directory ${this.file.path}`);
-      return;
-    }
-
     if (isWebExtension(this.addon.type)) {
       this.scope = Extension.getBootstrapScope(this.addon.id, this.file);
     } else if (this.addon.type === "webextension-langpack") {
       this.scope = Langpack.getBootstrapScope(this.addon.id, this.file);
     } else if (this.addon.type === "webextension-dictionary") {
       this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file);
     } else {
       let uri = getURIForResourceInFile(this.file, "bootstrap.js").spec;
 
+      let principal = Services.scriptSecurityManager.getSystemPrincipal();
       this.scope =
         new Cu.Sandbox(principal, { sandboxName: uri,
                                     addonId: this.addon.id,
                                     wantGlobalProperties: ["ChromeUtils"],
                                     metadata: { addonID: this.addon.id, URI: uri } });
 
       try {
-        // Copy the reason values from the global object into the bootstrap scope.
-        for (let name in BOOTSTRAP_REASONS)
-          this.scope[name] = BOOTSTRAP_REASONS[name];
-
-        // Add other stuff that extensions want.
-        Object.assign(this.scope, {Worker, ChromeWorker});
-
-        // Define a console for the add-on
+        Object.assign(this.scope, BOOTSTRAP_REASONS);
+
         XPCOMUtils.defineLazyGetter(
           this.scope, "console",
           () => new ConsoleAPI({ consoleID: `addon/${this.addon.id}` }));
 
-        this.scope.__SCRIPT_URI_SPEC__ = uri;
         Services.scriptloader.loadSubScript(uri, this.scope);
       } catch (e) {
         logger.warn(`Error loading bootstrap.js for ${this.addon.id}`, e);
       }
     }
 
     // Notify the BrowserToolboxProcess that a new addon has been loaded.
     let wrappedJSObject = { id: this.addon.id, options: { global: this.scope }};
@@ -1636,24 +2072,16 @@ class BootstrapScope {
 
 var XPIProvider = {
   get name() {
     return "XPIProvider";
   },
 
   BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),
 
-  // An array of known install locations
-  installLocations: null,
-  // A dictionary of known install locations by name
-  installLocationsByName: null,
-  // The value of the minCompatibleAppVersion preference
-  minCompatibleAppVersion: null,
-  // The value of the minCompatiblePlatformVersion preference
-  minCompatiblePlatformVersion: null,
   // A Map of active addons to their bootstrapScope by ID
   activeAddons: new Map(),
   // True if the platform could have activated extensions
   extensionsActive: false,
   // New distribution addons awaiting permissions approval
   newDistroAddons: null,
   // Keep track of startup phases for telemetry
   runPhase: XPI_STARTING,
@@ -1765,99 +2193,93 @@ var XPIProvider = {
         c.cancel();
       } catch (e) {
         logger.warn("Cancel failed", e);
       }
     }
   },
 
   setupInstallLocations(aAppChanged) {
-    function DirectoryLocation(aName, aScope, aKey, aPaths, aLocked) {
+    function DirectoryLoc(aName, aScope, aKey, aPaths, aLocked) {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       } catch (e) {
         return null;
       }
-      if (aLocked) {
-        return new DirectoryInstallLocation(aName, dir, aScope);
-      }
-      return new MutableDirectoryInstallLocation(aName, dir, aScope);
+      return new DirectoryLocation(aName, dir, aScope, aLocked);
     }
 
-    function BuiltInLocation(name, scope, key, paths) {
+    function BuiltInLoc(name, scope, key, paths) {
       try {
         var dir = FileUtils.getDir(key, paths);
       } catch (e) {
         return null;
       }
-      return new BuiltInInstallLocation(name, dir, scope);
+      return new BuiltInLocation(name, dir, scope);
     }
 
-    function SystemLocation(aName, aScope, aKey, aPaths) {
+    function SystemLoc(aName, aScope, aKey, aPaths) {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       } catch (e) {
         return null;
       }
-      return new SystemAddonInstallLocation(aName, dir, aScope, aAppChanged !== false);
+      return new SystemAddonLocation(aName, dir, aScope, aAppChanged !== false);
     }
 
-    function RegistryLocation(aName, aScope, aKey) {
+    function RegistryLoc(aName, aScope, aKey) {
       if ("nsIWindowsRegKey" in Ci) {
-        return new WinRegInstallLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
+        return new WinRegLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
       }
     }
 
     let enabledScopes = Services.prefs.getIntPref(PREF_EM_ENABLED_SCOPES,
                                                   AddonManager.SCOPE_ALL);
     // The profile location is always enabled
     enabledScopes |= AddonManager.SCOPE_PROFILE;
 
     // These must be in order of priority, highest to lowest,
     // for processFileChanges etc. to work
     let locations = [
       [() => TemporaryInstallLocation, TemporaryInstallLocation.name, null],
 
-      [DirectoryLocation, KEY_APP_PROFILE, AddonManager.SCOPE_PROFILE,
+      [DirectoryLoc, KEY_APP_PROFILE, AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR, [DIR_EXTENSIONS], false],
 
-      [SystemLocation, KEY_APP_SYSTEM_ADDONS, AddonManager.SCOPE_PROFILE,
+      [SystemLoc, KEY_APP_SYSTEM_ADDONS, AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR, [DIR_SYSTEM_ADDONS]],
 
-      [BuiltInLocation, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
+      [BuiltInLoc, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
        KEY_APP_FEATURES, []],
 
-      [DirectoryLocation, KEY_APP_SYSTEM_USER, AddonManager.SCOPE_USER,
+      [DirectoryLoc, KEY_APP_SYSTEM_USER, AddonManager.SCOPE_USER,
        "XREUSysExt", [Services.appinfo.ID], true],
 
-      [RegistryLocation, "winreg-app-user", AddonManager.SCOPE_USER,
+      [RegistryLoc, "winreg-app-user", AddonManager.SCOPE_USER,
        "ROOT_KEY_CURRENT_USER"],
 
-      [DirectoryLocation, KEY_APP_GLOBAL, AddonManager.SCOPE_APPLICATION,
+      [DirectoryLoc, KEY_APP_GLOBAL, AddonManager.SCOPE_APPLICATION,
        KEY_ADDON_APP_DIR, [DIR_EXTENSIONS], true],
 
-      [DirectoryLocation, KEY_APP_SYSTEM_SHARE, AddonManager.SCOPE_SYSTEM,
+      [DirectoryLoc, KEY_APP_SYSTEM_SHARE, AddonManager.SCOPE_SYSTEM,
        "XRESysSExtPD", [Services.appinfo.ID], true],
 
-      [DirectoryLocation, KEY_APP_SYSTEM_LOCAL, AddonManager.SCOPE_SYSTEM,
+      [DirectoryLoc, KEY_APP_SYSTEM_LOCAL, AddonManager.SCOPE_SYSTEM,
        "XRESysLExtPD", [Services.appinfo.ID], true],
 
-      [RegistryLocation, "winreg-app-global", AddonManager.SCOPE_SYSTEM,
+      [RegistryLoc, "winreg-app-global", AddonManager.SCOPE_SYSTEM,
        "ROOT_KEY_LOCAL_MACHINE"],
     ];
 
-    this.installLocations = [];
-    this.installLocationsByName = {};
     for (let [constructor, name, scope, ...args] of locations) {
       if (!scope || enabledScopes & scope) {
         try {
           let loc = constructor(name, scope, ...args);
           if (loc) {
-            this.installLocations.push(loc);
-            this.installLocationsByName[name] = loc;
+            XPIStates.addLocation(name, loc);
           }
         } catch (e) {
           logger.warn(`Failed to add ${constructor.name} install location ${name}`, e);
         }
       }
     }
   },
 
@@ -1886,23 +2308,16 @@ var XPIProvider = {
 
       // Clear this at startup for xpcshell test restarts
       this._telemetryDetails = {};
       // Register our details structure with AddonManager
       AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
 
       this.setupInstallLocations(aAppChanged);
 
-      this.minCompatibleAppVersion = Services.prefs.getStringPref(PREF_EM_MIN_COMPAT_APP_VERSION,
-                                                                  null);
-      this.minCompatiblePlatformVersion = Services.prefs.getStringPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
-                                                                       null);
-
-      Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this);
-      Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this);
       if (!AppConstants.MOZ_REQUIRE_SIGNING || Cu.isInAutomation)
         Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this);
       Services.prefs.addObserver(PREF_LANGPACK_SIGNATURES, this);
       Services.prefs.addObserver(PREF_ALLOW_LEGACY, this);
       Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS);
       Services.obs.addObserver(this, NOTIFICATION_TOOLBOX_CONNECTION_CHANGE);
 
 
@@ -1966,20 +2381,19 @@ var XPIProvider = {
             continue;
           }
 
           // If the add-on was pending disable then shut it down and remove it
           // from the persisted data.
           let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
           if (addon._pendingDisable) {
             reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
-          } else if (addon.location.name == KEY_APP_TEMPORARY) {
+          } else if (addon.location.isTemporary) {
             reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
-            let existing = XPIStates.findAddon(addon.id, loc =>
-              loc.name != TemporaryInstallLocation.name);
+            let existing = XPIStates.findAddon(addon.id, loc => !loc.isTemporary);
             if (existing) {
               reason = XPIInstall.newVersionReason(addon.version, existing.version);
             }
           }
           BootstrapScope.get(addon).shutdown(reason);
         }
         Services.obs.removeObserver(observer, "quit-application-granted");
       }, "quit-application-granted");
@@ -2068,48 +2482,43 @@ var XPIProvider = {
 
     // Ugh, if we reach this point without loading the xpi database,
     // we need to load it know, otherwise the telemetry shutdown blocker
     // will never resolve.
     if (!XPIDatabase.initialized) {
       await XPIDatabase.asyncLoadDB();
     }
 
-    this.installLocations = null;
-    this.installLocationsByName = null;
-
     // This is needed to allow xpcshell tests to simulate a restart
     this.extensionsActive = false;
 
     await XPIDatabase.shutdown();
   },
 
   cleanupTemporaryAddons() {
-    let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
-    if (tempLocation) {
-      for (let [id, addon] of tempLocation.entries()) {
-        tempLocation.delete(id);
-
-        let bootstrap = BootstrapScope.get(addon);
-        let existing = XPIStates.findAddon(id, loc => loc != tempLocation);
-
-        let cleanup = () => {
-          TemporaryInstallLocation.uninstallAddon(id);
-          XPIStates.removeAddon(TemporaryInstallLocation.name, id);
-        };
-
-        if (existing) {
-          bootstrap.update(existing, false, () => {
-            cleanup();
-            XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
-          });
-        } else {
-          bootstrap.uninstall();
+    let tempLocation = TemporaryInstallLocation;
+    for (let [id, addon] of tempLocation.entries()) {
+      tempLocation.delete(id);
+
+      let bootstrap = BootstrapScope.get(addon);
+      let existing = XPIStates.findAddon(id, loc => !loc.isTemporary);
+
+      let cleanup = () => {
+        tempLocation.installer.uninstallAddon(id);
+        tempLocation.removeAddon(id);
+      };
+
+      if (existing) {
+        bootstrap.update(existing, false, () => {
           cleanup();
-        }
+          XPIDatabase.makeAddonLocationVisible(id, existing.location);
+        });
+      } else {
+        bootstrap.uninstall();
+        cleanup();
       }
     }
   },
 
   /**
    * Adds a list of currently active add-ons to the next crash report.
    */
   addAddonsToCrashReporter() {
@@ -2140,53 +2549,51 @@ var XPIProvider = {
    * @param {Object} aManifests
    *         A dictionary to add detected install manifests to for the purpose
    *         of passing through updated compatibility information
    * @returns {boolean}
    *        True if an add-on was installed or uninstalled
    */
   processPendingFileChanges(aManifests) {
     let changed = false;
-    for (let location of this.installLocations) {
-      aManifests[location.name] = {};
+    for (let loc of XPIStates.locations()) {
+      aManifests[loc.name] = {};
       // We can't install or uninstall anything in locked locations
-      if (location.locked) {
+      if (loc.locked) {
         continue;
       }
 
-      let state = XPIStates.getLocation(location.name);
-
       let cleanNames = [];
       let promises = [];
-      for (let [id, metadata] of state.getStagedAddons()) {
-        state.unstageAddon(id);
-
-        aManifests[location.name][id] = null;
+      for (let [id, metadata] of loc.getStagedAddons()) {
+        loc.unstageAddon(id);
+
+        aManifests[loc.name][id] = null;
         promises.push(
-          XPIInstall.installStagedAddon(id, metadata, location).then(
+          XPIInstall.installStagedAddon(id, metadata, loc).then(
             addon => {
-              aManifests[location.name][id] = addon;
+              aManifests[loc.name][id] = addon;
             },
             error => {
-              delete aManifests[location.name][id];
+              delete aManifests[loc.name][id];
               cleanNames.push(`${id}.xpi`);
 
-              logger.error(`Failed to install staged add-on ${id} in ${location.name}`,
+              logger.error(`Failed to install staged add-on ${id} in ${loc.name}`,
                            error);
             }));
       }
 
       if (promises.length) {
         changed = true;
         awaitPromise(Promise.all(promises));
       }
 
       try {
         if (cleanNames.length) {
-          location.cleanStagingDir(cleanNames);
+          loc.installer.cleanStagingDir(cleanNames);
         }
       } catch (e) {
         // Non-critical, just saves some perf on startup if we clean this up.
         logger.debug("Error cleaning staging dir", e);
       }
     }
     return changed;
   },
@@ -2204,69 +2611,61 @@ var XPIProvider = {
    *        See checkForChanges
    * @returns {boolean}
    *        True if any new add-ons were installed
    */
   installDistributionAddons(aManifests, aAppChanged) {
     let distroDir;
     try {
       distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
-      if (!distroDir.isDirectory())
-        return false;
     } catch (e) {
       return false;
     }
 
     let changed = false;
-    let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
-
-    let entries = distroDir.directoryEntries
-                           .QueryInterface(Ci.nsIDirectoryEnumerator);
-    let entry;
-    while ((entry = entries.nextFile)) {
-
-      let id = entry.leafName;
+    let profileLocation = XPIStates.getLocation(KEY_APP_PROFILE);
+
+    for (let file of iterDirectory(distroDir)) {
+      let id = file.leafName;
       if (id.endsWith(".xpi")) {
         id = id.slice(0, -4);
       } else {
-        logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+        logger.debug(`Ignoring distribution add-on that isn't an XPI: ${file.path}`);
         continue;
       }
 
       if (!gIDTest.test(id)) {
         logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
-            entry.path);
+                     file.path);
         continue;
       }
 
       /* If this is not an upgrade and we've already handled this extension
        * just continue */
       if (!aAppChanged && Services.prefs.prefHasUserValue(PREF_BRANCH_INSTALLED_ADDON + id)) {
         continue;
       }
 
       try {
-        let addon = awaitPromise(XPIInstall.installDistributionAddon(id, entry, profileLocation));
+        let addon = awaitPromise(XPIInstall.installDistributionAddon(id, file, profileLocation));
 
         if (addon) {
           // aManifests may contain a copy of a newly installed add-on's manifest
           // and we'll have overwritten that so instead cache our install manifest
           // which will later be put into the database in processFileChanges
           if (!(KEY_APP_PROFILE in aManifests))
             aManifests[KEY_APP_PROFILE] = {};
           aManifests[KEY_APP_PROFILE][id] = addon;
           changed = true;
         }
       } catch (e) {
-        logger.error("Failed to install distribution add-on " + entry.path, e);
+        logger.error(`Failed to install distribution add-on ${file.path}`, e);
       }
     }
 
-    entries.close();
-
     return changed;
   },
 
   getNewDistroAddons() {
     let addons = this.newDistroAddons;
     this.newDistroAddons = null;
     return addons;
   },
@@ -2309,17 +2708,17 @@ var XPIProvider = {
 
     // Keep track of whether and why we need to open and update the database at
     // startup time.
     let updateReasons = [];
     if (aAppChanged) {
       updateReasons.push("appChanged");
     }
 
-    let installChanged = XPIStates.getInstallState(aAppChanged === false);
+    let installChanged = XPIStates.scanForChanges(aAppChanged === false);
     if (installChanged) {
       updateReasons.push("directoryState");
     }
 
     // First install any new add-ons into the locations, if there are any
     // changes then we must update the database with the information in the
     // install locations
     let manifests = {};
@@ -2409,17 +2808,17 @@ var XPIProvider = {
   /**
    * Gets an array of add-ons which were placed in a known install location
    * prior to startup of the current session, were detected by a directory scan
    * of those locations, and are currently disabled.
    *
    * @returns {Promise<Array<Addon>>}
    */
   async getNewSideloads() {
-    if (XPIStates.getInstallState(false)) {
+    if (XPIStates.scanForChanges(false)) {
       // We detected changes. Update the database to account for them.
       await XPIDatabase.asyncLoadDB(false);
       XPIDatabaseReconcile.processFileChanges({}, false);
       this._updateActiveAddons();
     }
 
     let addons = await Promise.all(
       Array.from(XPIStates.sideLoadedAddons.keys(),
@@ -2569,17 +2968,17 @@ var XPIProvider = {
       throw new Error("XPIStates not yet initialized");
     }
 
     let result = [];
     for (let addon of XPIStates.enabledAddons()) {
       if (aTypes && !aTypes.includes(addon.type)) {
         continue;
       }
-      let location = this.installLocationsByName[addon.location.name];
+      let {location} = addon;
       let scope, isSystem;
       if (location) {
         ({scope, isSystem} = location);
       }
       result.push({
         id: addon.id,
         version: addon.version,
         type: addon.type,
@@ -2680,571 +3079,58 @@ var XPIProvider = {
       return;
     } else if (aTopic == NOTIFICATION_TOOLBOX_CONNECTION_CHANGE) {
       this.onDebugConnectionChange(aSubject.wrappedJSObject);
       return;
     }
 
     if (aTopic == "nsPref:changed") {
       switch (aData) {
-      case PREF_EM_MIN_COMPAT_APP_VERSION:
-        this.minCompatibleAppVersion = Services.prefs.getStringPref(PREF_EM_MIN_COMPAT_APP_VERSION,
-                                                                    null);
-        this.updateAddonAppDisabledStates();
-        break;
-      case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
-        this.minCompatiblePlatformVersion = Services.prefs.getStringPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
-                                                                         null);
-        this.updateAddonAppDisabledStates();
-        break;
       case PREF_XPI_SIGNATURES_REQUIRED:
       case PREF_LANGPACK_SIGNATURES:
       case PREF_ALLOW_LEGACY:
         this.updateAddonAppDisabledStates();
         break;
       }
     }
   },
 };
 
-for (let meth of ["cancelUninstallAddon", "getInstallForFile",
-                  "getInstallForURL", "getInstallsByTypes",
+for (let meth of ["getInstallForFile", "getInstallForURL", "getInstallsByTypes",
                   "installTemporaryAddon", "isInstallAllowed",
-                  "isInstallEnabled", "uninstallAddon",
-                  "updateSystemAddons"]) {
+                  "isInstallEnabled", "updateSystemAddons"]) {
   XPIProvider[meth] = function() {
     return XPIInstall[meth](...arguments);
   };
 }
 
-function forwardInstallMethods(cls, methods) {
-  let {prototype} = cls;
-  for (let meth of methods) {
-    prototype[meth] = function() {
-      return XPIInstall[cls.name].prototype[meth].apply(this, arguments);
-    };
-  }
-}
-
-/**
- * An object which identifies a directory install location for add-ons. The
- * location consists of a directory which contains the add-ons installed in the
- * location.
- *
- */
-class DirectoryInstallLocation {
-  /**
-   * Each add-on installed in the location is either a directory containing the
-   * add-on's files or a text file containing an absolute path to the directory
-   * containing the add-ons files. The directory or text file must have the same
-   * name as the add-on's ID.
-   *
-   * @param {string} aName
-   *        The string identifier for the install location
-   * @param {nsIFile} aDirectory
-   *        The nsIFile directory for the install location
-   * @param {integer} aScope
-   *        The scope of add-ons installed in this location
-  */
-  constructor(aName, aDirectory, aScope) {
-    this._name = aName;
-    this.locked = true;
-    this._directory = aDirectory;
-    this._scope = aScope;
-    this._IDToFileMap = {};
-    this._linkedAddons = [];
-
-    this.isSystem = (aName == KEY_APP_SYSTEM_ADDONS ||
-                     aName == KEY_APP_SYSTEM_DEFAULTS);
-
-    if (!aDirectory || !aDirectory.exists())
-      return;
-    if (!aDirectory.isDirectory())
-      throw new Error("Location must be a directory.");
-
-    this.initialized = false;
-  }
-
-  get path() {
-    return this._directory && this._directory.path;
-  }
-
-  /**
-   * Reads a directory linked to in a file.
-   *
-   * @param {nsIFile} aFile
-   *        The file containing the directory path
-   * @returns {nsIFile?}
-   *        An nsIFile object representing the linked directory, or null
-   *        on error.
-   */
-  _readDirectoryFromFile(aFile) {
-    let linkedDirectory;
-    if (aFile.isSymlink()) {
-      linkedDirectory = aFile.clone();
-      try {
-        linkedDirectory.normalize();
-      } catch (e) {
-        logger.warn("Symbolic link " + aFile.path + " points to a path" +
-             " which does not exist");
-        return null;
-      }
-    } else {
-      let fis = new FileInputStream(aFile, -1, -1, false);
-      let line = { value: "" };
-      fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
-      fis.close();
-      if (line.value) {
-        linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-        try {
-          linkedDirectory.initWithPath(line.value);
-        } catch (e) {
-          linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
-        }
-      }
-    }
-
-    if (linkedDirectory) {
-      if (!linkedDirectory.exists()) {
-        logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
-             " which does not exist");
-        return null;
-      }
-
-      if (!linkedDirectory.isDirectory()) {
-        logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
-             " which is not a directory");
-        return null;
-      }
-
-      return linkedDirectory;
-    }
-
-    logger.warn("File pointer " + aFile.path + " does not contain a path");
-    return null;
-  }
-
-  /**
-   * Finds all the add-ons installed in this location.
-   *
-   * @param {boolean} [rescan = false]
-   *        True if the directory should be re-scanned, even if it has
-   *        already been initialized.
-   */
-  _readAddons(rescan = false) {
-    if ((this.initialized && !rescan) || !this._directory) {
-      return;
-    }
-    this.initialized = true;
-
-    // Use a snapshot of the directory contents to avoid possible issues with
-    // iterating over a directory while removing files from it (the YAFFS2
-    // embedded filesystem has this issue, see bug 772238).
-    let entries = getDirectoryEntries(this._directory);
-    for (let entry of entries) {
-      let id = entry.leafName;
-
-      if (id == DIR_STAGE || id == DIR_TRASH)
-        continue;
-
-      let isFile = id.toLowerCase().endsWith(".xpi");
-      if (isFile) {
-        id = id.substring(0, id.length - 4);
-      }
-
-      if (!gIDTest.test(id)) {
-        logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
-                     entry.path);
-        continue;
-      }
-
-      if (!isFile && (entry.isFile() || entry.isSymlink())) {
-        let newEntry = this._readDirectoryFromFile(entry);
-        if (!newEntry) {
-          logger.debug("Deleting stale pointer file " + entry.path);
-          try {
-            entry.remove(true);
-          } catch (e) {
-            logger.warn("Failed to remove stale pointer file " + entry.path, e);
-            // Failing to remove the stale pointer file is ignorable
-          }
-          continue;
-        }
-
-        entry = newEntry;
-        this._linkedAddons.push(id);
-      }
-
-      this._IDToFileMap[id] = entry;
-    }
-  }
-
-  /**
-   * Gets the name of this install location.
-   */
-  get name() {
-    return this._name;
-  }
-
-  /**
-   * Gets the scope of this install location.
-   */
-  get scope() {
-    return this._scope;
-  }
-
-  /**
-   * Gets an map of files for add-ons installed in this location.
-   *
-   * @param {boolean} [rescan = false]
-   *        True if the directory should be re-scanned, even if it has
-   *        already been initialized.
-   *
-   * @returns {Map<string, nsIFile>}
-   *        A map of all add-ons in the location, with each add-on's ID
-   *        as the key and an nsIFile for its location as the value.
-   */
-  getAddonLocations(rescan = false) {
-    this._readAddons(rescan);
-
-    let locations = new Map();
-    for (let id in this._IDToFileMap) {
-      locations.set(id, this._IDToFileMap[id].clone());
-    }
-    return locations;
-  }
-
-  /**
-   * Gets the directory that the add-on with the given ID is installed in.
-   *
-   * @param {string} aId
-   *        The ID of the add-on
-   * @returns {nsIFile}
-   * @throws if the ID does not match any of the add-ons installed
-   */
-  getLocationForID(aId) {
-    if (!(aId in this._IDToFileMap))
-      this._readAddons();
-
-    if (aId in this._IDToFileMap)
-      return this._IDToFileMap[aId].clone();
-    throw new Error("Unknown add-on ID " + aId);
-  }
-
-  /**
-   * Returns true if the given addon was installed in this location by a text
-   * file pointing to its real path.
-   *
-   * @param {string} aId
-   *        The ID of the addon
-   * @returns {boolean}
-   */
-  isLinkedAddon(aId) {
-    return this._linkedAddons.includes(aId);
-  }
-}
-
-/**
- * An extension of DirectoryInstallLocation which adds methods to installing
- * and removing add-ons from the directory at runtime.
- */
-class MutableDirectoryInstallLocation extends DirectoryInstallLocation {
-  /**
-   * @param {string} aName
-   *        The string identifier for the install location
-   * @param {nsIFile} aDirectory
-   *        The nsIFile directory for the install location
-   * @param {integer} aScope
-   *        The scope of add-ons installed in this location
-   */
-  constructor(aName, aDirectory, aScope) {
-    super(aName, aDirectory, aScope);
-
-    this.locked = false;
-    this._stagingDirLock = 0;
-  }
-}
-forwardInstallMethods(MutableDirectoryInstallLocation,
-                      ["cleanStagingDir", "getStagingDir", "getTrashDir",
-                       "installAddon", "releaseStagingDir", "requestStagingDir",
-                       "uninstallAddon"]);
-
-/**
- * An object which identifies a built-in install location for add-ons, such
- * as default system add-ons.
- *
- * This location should point either to a XPI, or a directory in a local build.
- */
-class BuiltInInstallLocation extends DirectoryInstallLocation {
-  /**
-   * Read the manifest of allowed add-ons and build a mapping between ID and URI
-   * for each.
-   */
-  _readAddons() {
-    let manifest;
-    try {
-      let url = Services.io.newURI(BUILT_IN_ADDONS_URI);
-      let data = Cu.readUTF8URI(url);
-      manifest = JSON.parse(data);
-    } catch (e) {
-      logger.warn("List of valid built-in add-ons could not be parsed.", e);
-      return;
-    }
-
-    if (!("system" in manifest)) {
-      logger.warn("No list of valid system add-ons found.");
-      return;
-    }
-
-    for (let id of manifest.system) {
-      let file = new FileUtils.File(this._directory.path);
-      file.append(`${id}.xpi`);
-
-      // Only attempt to load unpacked directory if unofficial build.
-      if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
-        file = new FileUtils.File(this._directory.path);
-        file.append(`${id}`);
-      }
-
-      this._IDToFileMap[id] = file;
-    }
-  }
-}
-
-/**
- * An object which identifies a directory install location for system add-ons
- * updates.
- */
-class SystemAddonInstallLocation extends MutableDirectoryInstallLocation {
-  /**
-    * The location consists of a directory which contains the add-ons installed.
-    *
-    * @param {string} aName
-    *        The string identifier for the install location
-    * @param {nsIFile} aDirectory
-    *        The nsIFile directory for the install location
-    * @param {integer} aScope
-    *        The scope of add-ons installed in this location
-    * @param {boolean} aResetSet
-    *        True to throw away the current add-on set
-    */
-  constructor(aName, aDirectory, aScope, aResetSet) {
-    let addonSet = SystemAddonInstallLocation._loadAddonSet();
-    let directory = null;
-
-    // The system add-on update directory is stored in a pref.
-    // Therefore, this is looked up before calling the
-    // constructor on the superclass.
-    if (addonSet.directory) {
-      directory = getFile(addonSet.directory, aDirectory);
-      logger.info("SystemAddonInstallLocation scanning directory " + directory.path);
-    } else {
-      logger.info("SystemAddonInstallLocation directory is missing");
-    }
-
-    super(aName, directory, aScope);
-
-    this._addonSet = addonSet;
-    this._baseDir = aDirectory;
-    this._nextDir = null;
-    this._directory = directory;
-
-    this._stagingDirLock = 0;
-
-    if (aResetSet) {
-      this.resetAddonSet();
-    }
-
-    this.locked = false;
-  }
-
-  /**
-   * Reads the current set of system add-ons
-   *
-   * @returns {Object}
-   */
-  static _loadAddonSet() {
-    try {
-      let setStr = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_SET, null);
-      if (setStr) {
-        let addonSet = JSON.parse(setStr);
-        if ((typeof addonSet == "object") && addonSet.schema == 1) {
-          return addonSet;
-        }
-      }
-    } catch (e) {
-      logger.error("Malformed system add-on set, resetting.");
-    }
-
-    return { schema: 1, addons: {} };
-  }
-
-  getAddonLocations() {
-    // Updated system add-ons are ignored in safe mode
-    if (Services.appinfo.inSafeMode) {
-      return new Map();
-    }
-
-    let addons = super.getAddonLocations();
-
-    // Strip out any unexpected add-ons from the list
-    for (let id of addons.keys()) {
-      if (!(id in this._addonSet.addons)) {
-        addons.delete(id);
-      }
-    }
-
-    return addons;
-  }
-
-  /**
-   * Tests whether updated system add-ons are expected.
-   *
-   * @returns {boolean}
-   */
-  isActive() {
-    return this._directory != null;
-  }
-}
-
-forwardInstallMethods(SystemAddonInstallLocation,
-                      ["cleanDirectories", "cleanStagingDir", "getStagingDir",
-                       "getTrashDir", "installAddon", "installAddon",
-                       "installAddonSet", "isValid", "isValidAddon",
-                       "releaseStagingDir", "requestStagingDir",
-                       "resetAddonSet", "resumeAddonSet", "uninstallAddon",
-                       "uninstallAddon"]);
-
-/** An object which identifies an install location for temporary add-ons.
- */
-const TemporaryInstallLocation = { locked: false, name: KEY_APP_TEMPORARY,
-  scope: AddonManager.SCOPE_TEMPORARY,
-  getAddonLocations: () => [], isLinkedAddon: () => false, installAddon:
-    () => {}, uninstallAddon: (aAddon) => {}, getStagingDir: () => {},
-};
-
-/**
- * An object that identifies a registry install location for add-ons. The location
- * consists of a registry key which contains string values mapping ID to the
- * path where an add-on is installed
- *
- */
-class WinRegInstallLocation extends DirectoryInstallLocation {
-  /**
-    * @param {string} aName
-    *        The string identifier of this Install Location.
-    * @param {integer} aRootKey
-    *        The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
-    * @param {integer} aScope
-    *        The scope of add-ons installed in this location
-    */
-  constructor(aName, aRootKey, aScope) {
-    super(aName, undefined, aScope);
-
-    this.locked = true;
-    this._name = aName;
-    this._rootKey = aRootKey;
-    this._scope = aScope;
-    this._IDToFileMap = {};
-
-    let path = this._appKeyPath + "\\Extensions";
-    let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(Ci.nsIWindowsRegKey);
-
-    // Reading the registry may throw an exception, and that's ok.  In error
-    // cases, we just leave ourselves in the empty state.
-    try {
-      key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
-    } catch (e) {
-      return;
-    }
-
-    this._readAddons(key);
-    key.close();
-  }
-
-  /**
-   * Retrieves the path of this Application's data key in the registry.
-   */
-  get _appKeyPath() {
-    let appVendor = Services.appinfo.vendor;
-    let appName = Services.appinfo.name;
-
-    // XXX Thunderbird doesn't specify a vendor string
-    if (AppConstants.MOZ_APP_NAME == "thunderbird" && appVendor == "")
-      appVendor = "Mozilla";
-
-    // XULRunner-based apps may intentionally not specify a vendor
-    if (appVendor != "")
-      appVendor += "\\";
-
-    return "SOFTWARE\\" + appVendor + appName;
-  }
-
-  /**
-   * Read the registry and build a mapping between ID and path for each
-   * installed add-on.
-   *
-   * @param {nsIWindowsRegKey} aKey
-   *        The key that contains the ID to path mapping
-   */
-  _readAddons(aKey) {
-    let count = aKey.valueCount;
-    for (let i = 0; i < count; ++i) {
-      let id = aKey.getValueName(i);
-
-      let file = new nsIFile(aKey.readStringValue(id));
-
-      if (!file.exists()) {
-        logger.warn("Ignoring missing add-on in " + file.path);
-        continue;
-      }
-
-      this._IDToFileMap[id] = file;
-    }
-  }
-
-  /**
-   * Gets the name of this install location.
-   */
-  get name() {
-    return this._name;
-  }
-
-  /*
-   * @see DirectoryInstallLocation
-   */
-  isLinkedAddon(aId) {
-    return true;
-  }
-}
-
 var XPIInternal = {
   BOOTSTRAP_REASONS,
   BootstrapScope,
   DB_SCHEMA,
   KEY_APP_SYSTEM_ADDONS,
   KEY_APP_SYSTEM_DEFAULTS,
-  KEY_APP_TEMPORARY,
   PREF_BRANCH_INSTALLED_ADDON,
   PREF_SYSTEM_ADDON_SET,
   SIGNED_TYPES,
-  SystemAddonInstallLocation,
+  SystemAddonLocation,
   TEMPORARY_ADDON_SUFFIX,
   TOOLKIT_ID,
   TemporaryInstallLocation,
   XPIProvider,
   XPIStates,
   XPI_PERMISSION,
   awaitPromise,
   canRunInSafeMode,
   descriptorToPath,
   getExternalType,
   getURIForResourceInFile,
   isTheme,
   isWebExtension,
+  iterDirectory,
 };
 
 var addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     "type.extension.name",
                                     AddonManager.VIEW_TYPE_LIST, 4000,
                                     AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
--- a/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
@@ -13,17 +13,17 @@ async function test() {
   let aAddons = await AddonManager.getAllAddons();
   aAddons.sort(function compareTypeName(a, b) {
     return a.type.localeCompare(b.type) || a.name.localeCompare(b.name);
   });
 
   let allCompatible = true;
   for (let a of aAddons) {
     // Ignore plugins.
-    if (a.type == "plugin")
+    if (a.type == "plugin" || a.id == "workerbootstrap-test@mozilla.org")
       continue;
 
     ok(a.isCompatible, a.type + " " + a.name + " " + a.version + " should be compatible");
     allCompatible = allCompatible && a.isCompatible;
   }
 
   finish();
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -8,18 +8,16 @@ if (!_TEST_FILE[0].includes("toolkit/moz
   ok(false, ("head_addons.js may not be loaded by tests outside of " +
              "the add-on manager component."));
 }
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
 const PREF_EM_CHECK_UPDATE_SECURITY   = "extensions.checkUpdateSecurity";
 const PREF_EM_STRICT_COMPATIBILITY    = "extensions.strictCompatibility";
-const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
-const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
 const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
 const PREF_COMPAT_OVERRIDES              = "extensions.getAddons.compatOverides.url";
 const PREF_XPI_SIGNATURES_REQUIRED    = "xpinstall.signatures.required";
 const PREF_SYSTEM_ADDON_SET           = "extensions.systemAddonSet";
 const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
 const PREF_SYSTEM_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
 const PREF_DISABLE_SECURITY = ("security.turn_off_all_security_so_that_" +
                                "viruses_can_take_over_this_computer");
@@ -1122,20 +1120,16 @@ function promiseInstallWebExtension(aDat
       return installs[0].addon;
     return promise.then(() => installs[0].addon);
   });
 }
 
 // By default use strict compatibility
 Services.prefs.setBoolPref("extensions.strictCompatibility", true);
 
-// By default, set min compatible versions to 0
-Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
-Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
-
 // Ensure signature checks are enabled by default
 Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
 
 Services.prefs.setBoolPref("extensions.legacy.enabled", true);
 
 
 // Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml.
 function copyBlocklistToProfile(blocklistFile) {
--- a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -60,17 +60,17 @@ var lastTimestamp = Date.now();
  * @param aPath   File path to touch.
  * @param aChange True if we should notice the change, False if we shouldn't.
  */
 function checkChange(XS, aPath, aChange) {
   Assert.ok(aPath.exists());
   lastTimestamp += 10000;
   info("Touching file " + aPath.path + " with " + lastTimestamp);
   aPath.lastModifiedTime = lastTimestamp;
-  Assert.equal(XS.getInstallState(), aChange);
+  Assert.equal(XS.scanForChanges(), aChange);
   // Save the pref so we don't detect this change again
   XS.save();
 }
 
 // Get a reference to the XPIState (loaded by startupManager) so we can unit test it.
 function getXS() {
   let XPI = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
   return XPI.XPIStates;
@@ -90,17 +90,17 @@ add_task(async function detect_touches()
          ]);
 
   info("Disable test add-ons");
   pd.userDisabled = true;
 
   let XS = getXS();
 
   // Should be no changes detected here, because everything should start out up-to-date.
-  Assert.ok(!XS.getInstallState());
+  Assert.ok(!XS.scanForChanges());
 
   let states = XS.getLocation("app-profile");
 
   // State should correctly reflect enabled/disabled
   Assert.ok(states.get("packed-enabled@tests.mozilla.org").enabled);
   Assert.ok(!states.get("packed-disabled@tests.mozilla.org").enabled);
 
   // Touch various files and make sure the change is detected.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js
@@ -3,18 +3,16 @@
  */
 
 // This verifies that bootstrap.js has the expected globals defined
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
 
 const EXPECTED_GLOBALS = [
-  ["Worker", "function"],
-  ["ChromeWorker", "function"],
   ["console", "object"]
 ];
 
 async function run_test() {
   do_test_pending();
   await promiseStartupManager();
   let sawGlobals = false;
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
@@ -11,17 +11,17 @@ ChromeUtils.import("resource://gre/modul
 
 if (AppConstants.platform == "win" && AppConstants.DEBUG) {
   // Shutdown timing is flaky in this test, and remote extensions
   // sometimes wind up leaving the XPI locked at the point when we try
   // to remove it.
   Services.prefs.setBoolPref("extensions.webextensions.remote", false);
 }
 
-PromiseTestUtils.expectUncaughtRejection(/Message manager disconnected/);
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
 
 /* globals browser*/
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 const stageDir = profileDir.clone();
 stageDir.append("staged");
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js
@@ -20,17 +20,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "1",
         maxVersion: "1"
       }]
     },
     expected: {
       strictCompatibility: false,
     },
-    compatible: [true,  true,  true,  true],
+    compatible: {
+      nonStrict: true,
+      strict: true,
+    },
   },
 
   // Incompatible in strict compatibility mode
   "addon2@tests.mozilla.org": {
     "install.rdf": {
       id: "addon2@tests.mozilla.org",
       version: "1.0",
       name: "Test 2",
@@ -39,17 +42,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "0.7",
         maxVersion: "0.8"
       }]
     },
     expected: {
       strictCompatibility: false,
     },
-    compatible: [false, true,  false, true],
+    compatible: {
+      nonStrict: true,
+      strict: false,
+    },
   },
 
   // Opt-in to strict compatibility - always incompatible
   "addon4@tests.mozilla.org": {
     "install.rdf": {
       id: "addon4@tests.mozilla.org",
       version: "1.0",
       name: "Test 4",
@@ -59,17 +65,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "0.8",
         maxVersion: "0.9"
       }]
     },
     expected: {
       strictCompatibility: true,
     },
-    compatible: [false, false, false, false],
+    compatible: {
+      nonStrict: false,
+      strict: false,
+    },
   },
 
   // Addon from the future - would be marked as compatibile-by-default,
   // but minVersion is higher than the app version
   "addon5@tests.mozilla.org": {
     "install.rdf": {
       id: "addon5@tests.mozilla.org",
       version: "1.0",
@@ -79,17 +88,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "3",
         maxVersion: "5"
       }]
     },
     expected: {
       strictCompatibility: false,
     },
-    compatible: [false, false, false, false],
+    compatible: {
+      nonStrict: false,
+      strict: false,
+    },
   },
 
   // Extremely old addon - maxVersion is less than the minimum compat version
   // set in extensions.minCompatibleVersion
   "addon6@tests.mozilla.org": {
     "install.rdf": {
       id: "addon6@tests.mozilla.org",
       version: "1.0",
@@ -99,17 +111,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "0.1",
         maxVersion: "0.2"
       }]
     },
     expected: {
       strictCompatibility: false,
     },
-    compatible: [false, true,  false, false],
+    compatible: {
+      nonStrict: true,
+      strict: false,
+    },
   },
 
   // Dictionary - incompatible in strict compatibility mode
   "addon7@tests.mozilla.org": {
     "install.rdf": {
       id: "addon7@tests.mozilla.org",
       version: "1.0",
       name: "Test 7",
@@ -118,17 +133,20 @@ const ADDONS = {
         id: "xpcshell@tests.mozilla.org",
         minVersion: "0.8",
         maxVersion: "0.9"
       }]
     },
     expected: {
       strictCompatibility: false,
     },
-    compatible: [false, true,  false, true],
+    compatible: {
+      nonStrict: true,
+      strict: false,
+    },
   },
 };
 
 const IDS = Object.keys(ADDONS);
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
@@ -149,47 +167,33 @@ async function checkCompatStatus(strict,
 
 add_task(async function setup() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   for (let addon of Object.values(ADDONS)) {
     await promiseWriteInstallRDFForExtension(addon["install.rdf"], profileDir);
   }
 
-  Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0.1");
-
   await promiseStartupManager();
 });
 
-add_task(async function test_0() {
-  // Should default to enabling strict compat.
-  await checkCompatStatus(true, 0);
-});
-
 add_task(async function test_1() {
   info("Test 1");
   Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
-  await checkCompatStatus(false, 1);
+  await checkCompatStatus(false, "nonStrict");
   await promiseRestartManager();
-  await checkCompatStatus(false, 1);
+  await checkCompatStatus(false, "nonStrict");
 });
 
 add_task(async function test_2() {
   info("Test 2");
   Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
-  await checkCompatStatus(true, 2);
+  await checkCompatStatus(true, "strict");
   await promiseRestartManager();
-  await checkCompatStatus(true, 2);
-});
-
-add_task(async function test_3() {
-  info("Test 3");
-  Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
-  Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0.4");
-  await checkCompatStatus(false, 3);
+  await checkCompatStatus(true, "strict");
 });
 
 const CHECK_COMPAT_ADDONS = {
   "cc-addon1@tests.mozilla.org": {
     "install.rdf": {
       // Cannot be enabled as it has no target app info for the applciation
       id: "cc-addon1@tests.mozilla.org",
       version: "1.0",
--- a/xpcom/io/FileDescriptorFile.cpp
+++ b/xpcom/io/FileDescriptorFile.cpp
@@ -446,17 +446,17 @@ FileDescriptorFile::IsSpecial(bool* aRet
 
 NS_IMETHODIMP
 FileDescriptorFile::CreateUnique(uint32_t aType, uint32_t aAttributes)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
-FileDescriptorFile::GetDirectoryEntries(nsISimpleEnumerator** aEntries)
+FileDescriptorFile::GetDirectoryEntriesImpl(nsIDirectoryEnumerator** aEntries)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
 FileDescriptorFile::OpenANSIFileDesc(const char* aMode, FILE** aRetVal)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
--- a/xpcom/io/nsIDirectoryEnumerator.idl
+++ b/xpcom/io/nsIDirectoryEnumerator.idl
@@ -1,25 +1,26 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ 
 /* 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"
+#include "nsISimpleEnumerator.idl"
 
 interface nsIFile;
 
 /**
  * This interface provides a means for enumerating the contents of a directory.
  * It is similar to nsISimpleEnumerator except the retrieved entries are QI'ed 
  * to nsIFile, and there is a mechanism for closing the directory when the 
  * enumeration is complete.
  */
 [scriptable, uuid(31f7f4ae-6916-4f2d-a81e-926a4e3022ee)]
-interface nsIDirectoryEnumerator : nsISupports
+interface nsIDirectoryEnumerator : nsISimpleEnumerator
 {
   /**
    * Retrieves the next file in the sequence. The "nextFile" element is the 
    * first element upon the first call. This attribute is null if there is no 
    * next element.
    */
   readonly attribute nsIFile nextFile;
 
--- a/xpcom/io/nsIFile.idl
+++ b/xpcom/io/nsIFile.idl
@@ -1,34 +1,34 @@
 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* 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"
+#include "nsIDirectoryEnumerator.idl"
 
 %{C++
 struct PRFileDesc;
 struct PRLibrary;
 #include <stdio.h>
 #include "mozilla/Path.h"
+#include "nsCOMPtr.h"
 #include "nsStringFwd.h"
 namespace mozilla {
 using PathString = nsTString<filesystem::Path::value_type>;
 using PathSubstring = nsTSubstring<filesystem::Path::value_type>;
 } // namespace mozilla
 %}
 
 [ptr] native PRFileDescStar(PRFileDesc);
 [ptr] native PRLibraryStar(PRLibrary);
 [ptr] native FILE(FILE);
 native PathString(mozilla::PathString);
 
-interface nsISimpleEnumerator;
-
 /**
  * An nsIFile is an abstract representation of a filename. It manages
  * filename encoding issues, pathname component separators ('/' vs. '\\'
  * vs. ':') and weird stuff like differing volumes with identical names, as
  * on pre-Darwin Macintoshes.
  *
  * This file has long introduced itself to new hackers with this opening
  * paragraph:
@@ -341,17 +341,33 @@ interface nsIFile : nsISupports
     
     /**
      *  Returns an enumeration of the elements in a directory. Each
      *  element in the enumeration is an nsIFile.
      *
      *   @throws NS_ERROR_FILE_NOT_DIRECTORY if the current nsIFile does
      *           not specify a directory.
      */
-    readonly attribute nsISimpleEnumerator directoryEntries;
+    [binaryname(DirectoryEntriesImpl)]
+    readonly attribute nsIDirectoryEnumerator directoryEntries;
+
+    %{C++
+    nsresult GetDirectoryEntries(nsISimpleEnumerator** aOut)
+    {
+      nsCOMPtr<nsIDirectoryEnumerator> dirEnum;
+      nsresult rv = GetDirectoryEntries(getter_AddRefs(dirEnum));
+      dirEnum.forget(aOut);
+      return rv;
+    };
+
+    nsresult GetDirectoryEntries(nsIDirectoryEnumerator** aOut)
+    {
+      return GetDirectoryEntriesImpl(aOut);
+    };
+    %}
 
     /**
      *  initWith[Native]Path
      *
      *  This function will initialize the nsIFile object.  Any
      *  internal state information will be reset.
      *
      *   @param filePath       
--- a/xpcom/io/nsLocalFileUnix.cpp
+++ b/xpcom/io/nsLocalFileUnix.cpp
@@ -83,18 +83,17 @@ using namespace mozilla;
 #define CHECK_mPath()                           \
     do {                                        \
         if (mPath.IsEmpty())                    \
             return NS_ERROR_NOT_INITIALIZED;    \
     } while(0)
 
 /* directory enumerator */
 class nsDirEnumeratorUnix final
-  : public nsISimpleEnumerator
-  , public nsIDirectoryEnumerator
+  : public nsIDirectoryEnumerator
 {
 public:
   nsDirEnumeratorUnix();
 
   // nsISupports interface
   NS_DECL_ISUPPORTS
 
   // nsISimpleEnumerator interface
@@ -1845,17 +1844,17 @@ nsLocalFile::GetFollowLinks(bool* aFollo
 
 NS_IMETHODIMP
 nsLocalFile::SetFollowLinks(bool aFollowLinks)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsLocalFile::GetDirectoryEntries(nsISimpleEnumerator** aEntries)
+nsLocalFile::GetDirectoryEntriesImpl(nsIDirectoryEnumerator** aEntries)
 {
   RefPtr<nsDirEnumeratorUnix> dir = new nsDirEnumeratorUnix();
 
   nsresult rv = dir->Init(this, false);
   if (NS_FAILED(rv)) {
     *aEntries = nullptr;
   } else {
     dir.forget(aEntries);
--- a/xpcom/io/nsLocalFileWin.cpp
+++ b/xpcom/io/nsLocalFileWin.cpp
@@ -225,23 +225,45 @@ private:
 
     return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
   }
 
   // Stores the path to perform the operation on
   nsString mResolvedPath;
 };
 
-class nsDriveEnumerator : public nsISimpleEnumerator
+class nsDriveEnumerator : public nsIDirectoryEnumerator
 {
 public:
   nsDriveEnumerator();
   NS_DECL_ISUPPORTS
   NS_DECL_NSISIMPLEENUMERATOR
   nsresult Init();
+
+  NS_IMETHOD GetNextFile(nsIFile** aResult) override
+  {
+    bool hasMore = false;
+    nsresult rv = HasMoreElements(&hasMore);
+    if (NS_FAILED(rv) || !hasMore) {
+      return rv;
+    }
+    nsCOMPtr<nsISupports> next;
+    rv = GetNext(getter_AddRefs(next));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIFile> result = do_QueryInterface(next);
+    result.forget(aResult);
+    return NS_OK;
+  }
+
+  NS_IMETHOD Close() override
+  {
+    return NS_OK;
+  }
+
 private:
   virtual ~nsDriveEnumerator();
 
   /* mDrives stores the null-separated drive names.
    * Init sets them.
    * HasMoreElements checks mStartOfCurrentDrive.
    * GetNext advances mStartOfCurrentDrive.
    */
@@ -653,18 +675,17 @@ CloseDir(nsDir*& aDir)
   return isOk ? NS_OK : ConvertWinError(GetLastError());
 }
 
 //-----------------------------------------------------------------------------
 // nsDirEnumerator
 //-----------------------------------------------------------------------------
 
 class nsDirEnumerator final
-  : public nsISimpleEnumerator
-  , public nsIDirectoryEnumerator
+  : public nsIDirectoryEnumerator
 {
 private:
   ~nsDirEnumerator()
   {
     Close();
   }
 
 public:
@@ -3069,17 +3090,17 @@ nsLocalFile::SetFollowLinks(bool aFollow
 {
   MakeDirty();
   mFollowSymlinks = aFollowLinks;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
-nsLocalFile::GetDirectoryEntries(nsISimpleEnumerator** aEntries)
+nsLocalFile::GetDirectoryEntriesImpl(nsIDirectoryEnumerator** aEntries)
 {
   nsresult rv;
 
   *aEntries = nullptr;
   if (mWorkingPath.EqualsLiteral("\\\\.")) {
     RefPtr<nsDriveEnumerator> drives = new nsDriveEnumerator;
     rv = drives->Init();
     if (NS_FAILED(rv)) {
@@ -3506,17 +3527,17 @@ nsLocalFile::GetHashCode(uint32_t* aResu
   // In order for short and long path names to hash to the same value we
   // always hash on the short pathname.
   EnsureShortPath();
 
   *aResult = HashString(mShortWorkingPath);
   return NS_OK;
 }
 
-NS_IMPL_ISUPPORTS(nsDriveEnumerator, nsISimpleEnumerator)
+NS_IMPL_ISUPPORTS(nsDriveEnumerator, nsIDirectoryEnumerator, nsISimpleEnumerator)
 
 nsDriveEnumerator::nsDriveEnumerator()
 {
 }
 
 nsDriveEnumerator::~nsDriveEnumerator()
 {
 }