Bug 899013 - Interface for customizing the DownloadIntegration module. r=mak draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 18 Apr 2016 11:26:24 +0100
changeset 352682 83201171c9b39d73c2b260fed1b1eb492b08f00b
parent 352681 d5af35cfdaacbfe2319adba174fb00a73f0b9cee
child 352683 fce47b199ea8a5f4cc846716fc6817904132d457
push id15745
push userpaolo.mozmail@amadzone.org
push dateMon, 18 Apr 2016 13:17:31 +0000
reviewersmak
bugs899013
milestone48.0a1
Bug 899013 - Interface for customizing the DownloadIntegration module. r=mak MozReview-Commit-ID: HWmcxXlLdLx
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/jsdownloads/src/Downloads.jsm
toolkit/components/jsdownloads/test/unit/common_test_Download.js
toolkit/components/jsdownloads/test/unit/head.js
toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -53,20 +53,19 @@ this.EXPORTED_SYMBOLS = [
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/Integration.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
-                                  "resource://gre/modules/DownloadIntegration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
@@ -85,16 +84,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
            Ci.nsPIExternalAppLauncher);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
 XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
            "@mozilla.org/gfx/printsettings-service;1",
            Ci.nsIPrintSettingsService);
 
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+            "resource://gre/modules/DownloadIntegration.jsm");
+
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
 /**
  * Returns true if the given value is a primitive string or a String object.
  */
 function isString(aValue) {
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -18,16 +18,17 @@ this.EXPORTED_SYMBOLS = [
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/Integration.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
                                   "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
@@ -84,16 +85,22 @@ XPCOMUtils.defineLazyGetter(this, "gPare
 XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
            "@mozilla.org/downloads/application-reputation-service;1",
            Ci.nsIApplicationReputationService);
 
 XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
                                    "@mozilla.org/telephony/volume-service;1",
                                    "nsIVolumeService");
 
+// We have to use the gCombinedDownloadIntegration identifier because, in this
+// module only, the DownloadIntegration identifier refers to the base version.
+Integration.downloads.defineModuleGetter(this, "gCombinedDownloadIntegration",
+            "resource://gre/modules/DownloadIntegration.jsm",
+            "DownloadIntegration");
+
 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
                                      "initWithCallback");
 
 /**
  * Indicates the delay between a change to the downloads data and the related
  * save operation.
  *
  * For best efficiency, this value should be high enough that the input/output
@@ -142,197 +149,170 @@ const kVerdictMap = {
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadIntegration
 
 /**
  * Provides functions to integrate with the host application, handling for
  * example the global prompts on shutdown.
  */
 this.DownloadIntegration = {
-  // For testing only
-  _testMode: false,
-  testPromptDownloads: 0,
-  dontLoadList: false,
-  dontLoadObservers: false,
-  dontCheckParentalControls: false,
-  shouldBlockInTest: false,
-  dontCheckRuntimePermissions: false,
-  shouldBlockInTestForRuntimePermissions: false,
-#ifdef MOZ_URL_CLASSIFIER
-  dontCheckApplicationReputation: false,
-#else
-  dontCheckApplicationReputation: true,
-#endif
-  shouldBlockInTestForApplicationReputation: false,
-  verdictInTestForApplicationReputation: "",
-  shouldKeepBlockedDataInTest: false,
-  dontOpenFileAndFolder: false,
-  downloadDoneCalled: false,
-  _deferTestOpenFile: null,
-  _deferTestShowDir: null,
-  _deferTestClearPrivateList: null,
-
   /**
    * Main DownloadStore object for loading and saving the list of persistent
    * downloads, or null if the download list was never requested and thus it
    * doesn't need to be persisted.
    */
   _store: null,
 
   /**
-   * Gets and sets test mode
-   */
-  get testMode() {
-    return this._testMode;
-  },
-  set testMode(mode) {
-    this._downloadsDirectory = null;
-    return (this._testMode = mode);
-  },
-
-  /**
    * Returns whether data for blocked downloads should be kept on disk.
    * Implementations which support unblocking downloads may return true to
    * keep the blocked download on disk until its fate is decided.
    *
    * If a download is blocked and the partial data is kept the Download's
    * 'hasBlockedData' property will be true. In this state Download.unblock()
    * or Download.confirmBlock() may be used to either unblock the download or
    * remove the downloaded data respectively.
    *
    * Even if shouldKeepBlockedData returns true, if the download did not use a
    * partFile the blocked data will be removed - preventing the complete
    * download from existing on disk with its final filename.
    *
    * @return boolean True if data should be kept.
    */
-  shouldKeepBlockedData: function() {
-    if (this.shouldBlockInTestForApplicationReputation) {
-      return this.shouldKeepBlockedDataInTest;
-    }
-
+  shouldKeepBlockedData() {
     const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
     return Services.appinfo.ID == FIREFOX_ID;
   },
 
   /**
    * Performs initialization of the list of persistent downloads, before its
    * first use by the host application.  This function may be called only once
    * during the entire lifetime of the application.
    *
-   * @param aList
+   * @param list
+   *        DownloadList object to be initialized.
+   *
+   * @return {Promise}
+   * @resolves When the list has been initialized.
+   * @rejects JavaScript exception.
+   */
+  initializePublicDownloadList: Task.async(function* (list) {
+    try {
+      yield this.loadPublicDownloadListFromStore(list);
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+
+    // After the list of persistent downloads has been loaded, we can add the
+    // history observers, even if the load operation failed. This object is kept
+    // alive by the history service.
+    new DownloadHistoryObserver(list);
+  }),
+
+  /**
+   * Called by initializePublicDownloadList to load the list of persistent
+   * downloads, before its first use by the host application.  This function may
+   * be called only once during the entire lifetime of the application.
+   *
+   * @param list
    *        DownloadList object to be populated with the download objects
    *        serialized from the previous session.  This list will be persisted
    *        to disk during the session lifetime.
    *
    * @return {Promise}
    * @resolves When the list has been populated.
    * @rejects JavaScript exception.
    */
-  initializePublicDownloadList: function(aList) {
-    return Task.spawn(function task_DI_initializePublicDownloadList() {
-      if (this.dontLoadList) {
-        // In tests, only register the history observer.  This object is kept
-        // alive by the history service, so we don't keep a reference to it.
-        new DownloadHistoryObserver(aList);
-        return;
-      }
+  loadPublicDownloadListFromStore: Task.async(function* (list) {
+    if (this._store) {
+      throw new Error("Initialization may be performed only once.");
+    }
 
-      if (this._store) {
-        throw new Error("initializePublicDownloadList may be called only once.");
-      }
+    this._store = new DownloadStore(list, OS.Path.join(
+                                             OS.Constants.Path.profileDir,
+                                             "downloads.json"));
+    this._store.onsaveitem = this.shouldPersistDownload.bind(this);
 
-      this._store = new DownloadStore(aList, OS.Path.join(
-                                                OS.Constants.Path.profileDir,
-                                                "downloads.json"));
-      this._store.onsaveitem = this.shouldPersistDownload.bind(this);
-
+    try {
       if (this._importedFromSqlite) {
-        try {
-          yield this._store.load();
-        } catch (ex) {
-          Cu.reportError(ex);
-        }
+        yield this._store.load();
       } else {
         let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
                                         "downloads.sqlite");
 
         if (yield OS.File.exists(sqliteDBpath)) {
-          let sqliteImport = new DownloadImport(aList, sqliteDBpath);
+          let sqliteImport = new DownloadImport(list, sqliteDBpath);
           yield sqliteImport.import();
 
-          let importCount = (yield aList.getAll()).length;
+          let importCount = (yield list.getAll()).length;
           if (importCount > 0) {
             try {
               yield this._store.save();
             } catch (ex) { }
           }
 
           // No need to wait for the file removal.
           OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
         }
 
         Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
 
         // Don't even report error here because this file is pre Firefox 3
         // and most likely doesn't exist.
         OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
-                                    "downloads.rdf"));
+                                    "downloads.rdf")).catch(() => {});
 
       }
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
 
-      // After the list of persistent downloads has been loaded, add the
-      // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load
-      // operation failed).  These objects are kept alive by the underlying
-      // DownloadList and by the history service respectively.  We wait for a
-      // complete initialization of the view used for detecting changes to
-      // downloads to be persisted, before other callers get a chance to modify
-      // the list without being detected.
-      yield new DownloadAutoSaveView(aList, this._store).initialize();
-      new DownloadHistoryObserver(aList);
-    }.bind(this));
-  },
+    // Add the view used for detecting changes to downloads to be persisted.
+    // We must do this after the list of persistent downloads has been loaded,
+    // even if the load operation failed. We wait for a complete initialization
+    // so other callers cannot modify the list without being detected. The
+    // DownloadAutoSaveView is kept alive by the underlying DownloadList.
+    yield new DownloadAutoSaveView(list, this._store).initialize();
+  }),
 
 #ifdef MOZ_WIDGET_GONK
   /**
     * Finds the default download directory which can be either in the
     * internal storage or on the sdcard.
     *
     * @return {Promise}
     * @resolves The downloads directory string path.
     */
-  _getDefaultDownloadDirectory: function() {
-    return Task.spawn(function() {
-      let directoryPath;
-      let win = Services.wm.getMostRecentWindow("navigator:browser");
-      let storages = win.navigator.getDeviceStorages("sdcard");
-      let preferredStorageName;
-      // Use the first one or the default storage.
-      storages.forEach((aStorage) => {
-        if (aStorage.default || !preferredStorageName) {
-          preferredStorageName = aStorage.storageName;
-        }
-      });
-
-      // Now get the path for this storage area.
-      if (preferredStorageName) {
-        let volume = volumeService.getVolumeByName(preferredStorageName);
-        if (volume && volume.state === Ci.nsIVolume.STATE_MOUNTED){
-          directoryPath = OS.Path.join(volume.mountPoint, "downloads");
-          yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
-        }
-      }
-      if (directoryPath) {
-        throw new Task.Result(directoryPath);
-      } else {
-        throw new Components.Exception("No suitable storage for downloads.",
-                                       Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+  _getDefaultDownloadDirectory: Task.async(function* () {
+    let directoryPath;
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    let storages = win.navigator.getDeviceStorages("sdcard");
+    let preferredStorageName;
+    // Use the first one or the default storage.
+    storages.forEach((aStorage) => {
+      if (aStorage.default || !preferredStorageName) {
+        preferredStorageName = aStorage.storageName;
       }
     });
-  },
+
+    // Now get the path for this storage area.
+    if (preferredStorageName) {
+      let volume = volumeService.getVolumeByName(preferredStorageName);
+      if (volume && volume.state === Ci.nsIVolume.STATE_MOUNTED){
+        directoryPath = OS.Path.join(volume.mountPoint, "downloads");
+        yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
+      }
+    }
+    if (directoryPath) {
+      return directoryPath;
+    } else {
+      throw new Components.Exception("No suitable storage for downloads.",
+                                     Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+    }
+  }),
 #endif
 
   /**
    * Determines if a Download object from the list of persistent downloads
    * should be saved into a file, so that it can be restored across sessions.
    *
    * This function allows filtering out downloads that the host application is
    * not interested in persisting across sessions, for example downloads that
@@ -342,18 +322,17 @@ this.DownloadIntegration = {
    *        The Download object to be inspected.  This is originally taken from
    *        the global DownloadList object for downloads that were not started
    *        from a private browsing window.  The item may have been removed
    *        from the list since the save operation started, though in this case
    *        the save operation will be repeated later.
    *
    * @return True to save the download, false otherwise.
    */
-  shouldPersistDownload: function (aDownload)
-  {
+  shouldPersistDownload(aDownload) {
     // On all platforms, we save all the downloads currently in progress, as
     // well as stopped downloads for which we retained partially downloaded
     // data or we have blocked data.
     if (!aDownload.stopped || aDownload.hasPartialData ||
         aDownload.hasBlockedData) {
       return true;
     }
 #ifdef MOZ_B2G
@@ -372,147 +351,134 @@ this.DownloadIntegration = {
   },
 
   /**
    * Returns the system downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
-  getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
-    return Task.spawn(function() {
-      if (this._downloadsDirectory) {
-        // This explicitly makes this function a generator for Task.jsm. We
-        // need this because calls to the "yield" operator below may be
-        // preprocessed out on some platforms.
-        yield undefined;
-        throw new Task.Result(this._downloadsDirectory);
-      }
+  getSystemDownloadsDirectory: Task.async(function* () {
+    if (this._downloadsDirectory) {
+      return this._downloadsDirectory;
+    }
 
-      let directoryPath = null;
+    let directoryPath = null;
 #ifdef XP_MACOSX
-      directoryPath = this._getDirectory("DfltDwnld");
+    directoryPath = this._getDirectory("DfltDwnld");
 #elifdef XP_WIN
-      // For XP/2K, use My Documents/Downloads. Other version uses
-      // the default Downloads directory.
-      let version = parseFloat(Services.sysinfo.getProperty("version"));
-      if (version < 6) {
-        directoryPath = yield this._createDownloadsDirectory("Pers");
-      } else {
-        directoryPath = this._getDirectory("DfltDwnld");
-      }
+    // For XP/2K, use My Documents/Downloads. Other version uses
+    // the default Downloads directory.
+    let version = parseFloat(Services.sysinfo.getProperty("version"));
+    if (version < 6) {
+      directoryPath = yield this._createDownloadsDirectory("Pers");
+    } else {
+      directoryPath = this._getDirectory("DfltDwnld");
+    }
 #elifdef XP_UNIX
 #ifdef MOZ_WIDGET_ANDROID
-      // Android doesn't have a $HOME directory, and by default we only have
-      // write access to /data/data/org.mozilla.{$APP} and /sdcard
-      directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
-      if (!directoryPath) {
-        throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
-                                       Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
-      }
+    // Android doesn't have a $HOME directory, and by default we only have
+    // write access to /data/data/org.mozilla.{$APP} and /sdcard
+    directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
+    if (!directoryPath) {
+      throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
+                                     Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+    }
 #elifdef MOZ_WIDGET_GONK
-      directoryPath = this._getDefaultDownloadDirectory();
+    directoryPath = this._getDefaultDownloadDirectory();
 #else
-      // For Linux, use XDG download dir, with a fallback to Home/Downloads
-      // if the XDG user dirs are disabled.
-      try {
-        directoryPath = this._getDirectory("DfltDwnld");
-      } catch(e) {
-        directoryPath = yield this._createDownloadsDirectory("Home");
-      }
+    // For Linux, use XDG download dir, with a fallback to Home/Downloads
+    // if the XDG user dirs are disabled.
+    try {
+      directoryPath = this._getDirectory("DfltDwnld");
+    } catch(e) {
+      directoryPath = yield this._createDownloadsDirectory("Home");
+    }
 #endif
 #else
-      directoryPath = yield this._createDownloadsDirectory("Home");
+    directoryPath = yield this._createDownloadsDirectory("Home");
 #endif
-      this._downloadsDirectory = directoryPath;
-      throw new Task.Result(this._downloadsDirectory);
-    }.bind(this));
-  },
+
+    this._downloadsDirectory = directoryPath;
+    return this._downloadsDirectory;
+  }),
   _downloadsDirectory: null,
 
   /**
    * Returns the user downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
-  getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
-    return Task.spawn(function() {
-      let directoryPath = null;
+  getPreferredDownloadsDirectory: Task.async(function* () {
+    let directoryPath = null;
 #ifdef MOZ_WIDGET_GONK
-      directoryPath = this._getDefaultDownloadDirectory();
+    directoryPath = this._getDefaultDownloadDirectory();
 #else
-      let prefValue = 1;
+    let prefValue = 1;
+
+    try {
+      prefValue = Services.prefs.getIntPref("browser.download.folderList");
+    } catch(e) {}
 
-      try {
-        prefValue = Services.prefs.getIntPref("browser.download.folderList");
-      } catch(e) {}
-
-      switch(prefValue) {
-        case 0: // Desktop
-          directoryPath = this._getDirectory("Desk");
-          break;
-        case 1: // Downloads
+    switch(prefValue) {
+      case 0: // Desktop
+        directoryPath = this._getDirectory("Desk");
+        break;
+      case 1: // Downloads
+        directoryPath = yield this.getSystemDownloadsDirectory();
+        break;
+      case 2: // Custom
+        try {
+          let directory = Services.prefs.getComplexValue("browser.download.dir",
+                                                         Ci.nsIFile);
+          directoryPath = directory.path;
+          yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
+        } catch(ex) {
+          // Either the preference isn't set or the directory cannot be created.
           directoryPath = yield this.getSystemDownloadsDirectory();
-          break;
-        case 2: // Custom
-          try {
-            let directory = Services.prefs.getComplexValue("browser.download.dir",
-                                                           Ci.nsIFile);
-            directoryPath = directory.path;
-            yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
-          } catch(ex) {
-            // Either the preference isn't set or the directory cannot be created.
-            directoryPath = yield this.getSystemDownloadsDirectory();
-          }
-          break;
-        default:
-          directoryPath = yield this.getSystemDownloadsDirectory();
-      }
+        }
+        break;
+      default:
+        directoryPath = yield this.getSystemDownloadsDirectory();
+    }
 #endif
-      throw new Task.Result(directoryPath);
-    }.bind(this));
-  },
+    return directoryPath;
+  }),
 
   /**
    * Returns the temporary downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
-  getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
-    return Task.spawn(function() {
-      let directoryPath = null;
+  getTemporaryDownloadsDirectory: Task.async(function* () {
+    let directoryPath = null;
 #ifdef XP_MACOSX
-      directoryPath = yield this.getPreferredDownloadsDirectory();
+    directoryPath = yield this.getPreferredDownloadsDirectory();
 #elifdef MOZ_WIDGET_ANDROID
-      directoryPath = yield this.getSystemDownloadsDirectory();
+    directoryPath = yield this.getSystemDownloadsDirectory();
 #elifdef MOZ_WIDGET_GONK
-      directoryPath = yield this.getSystemDownloadsDirectory();
+    directoryPath = yield this.getSystemDownloadsDirectory();
 #else
-      directoryPath = this._getDirectory("TmpD");
+    directoryPath = this._getDirectory("TmpD");
 #endif
-      throw new Task.Result(directoryPath);
-    }.bind(this));
-  },
+    return directoryPath;
+  }),
 
   /**
    * Checks to determine whether to block downloads for parental controls.
    *
    * aParam aDownload
    *        The download object.
    *
    * @return {Promise}
    * @resolves The boolean indicates to block downloads or not.
    */
-  shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) {
-    if (this.dontCheckParentalControls) {
-      return Promise.resolve(this.shouldBlockInTest);
-    }
-
+  shouldBlockForParentalControls(aDownload) {
     let isEnabled = gParentalControlsService &&
                     gParentalControlsService.parentalControlsEnabled;
     let shouldBlock = isEnabled &&
                       gParentalControlsService.blockFileDownloadsEnabled;
 
     // Log the event if required by parental controls settings.
     if (isEnabled && gParentalControlsService.loggingEnabled) {
       gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
@@ -524,21 +490,17 @@ this.DownloadIntegration = {
   },
 
   /**
    * Checks to determine whether to block downloads for not granted runtime permissions.
    *
    * @return {Promise}
    * @resolves The boolean indicates to block downloads or not.
    */
-  shouldBlockForRuntimePermissions: function DI_shouldBlockForRuntimePermissions() {
-    if (this.dontCheckRuntimePermissions) {
-      return Promise.resolve(this.shouldBlockInTestForRuntimePermissions);
-    }
-
+  shouldBlockForRuntimePermissions() {
 #ifdef MOZ_WIDGET_ANDROID
     return RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE)
                              .then(permissionGranted => !permissionGranted);
 #else
     return Promise.resolve(false);
 #endif
   },
 
@@ -553,23 +515,23 @@ this.DownloadIntegration = {
    * @resolves Object with the following properties:
    *           {
    *             shouldBlock: Whether the download should be blocked.
    *             verdict: Detailed reason for the block, according to the
    *                      "Downloads.Error.BLOCK_VERDICT_" constants, or empty
    *                      string if the reason is unknown.
    *           }
    */
-  shouldBlockForReputationCheck: function (aDownload) {
-    if (this.dontCheckApplicationReputation) {
-      return Promise.resolve({
-        shouldBlock: this.shouldBlockInTestForApplicationReputation,
-        verdict: this.verdictInTestForApplicationReputation,
-      });
-    }
+  shouldBlockForReputationCheck(aDownload) {
+#ifndef MOZ_URL_CLASSIFIER
+    return Promise.resolve({
+      shouldBlock: false,
+      verdict: "",
+    });
+#else
     let hash;
     let sigInfo;
     let channelRedirects;
     try {
       hash = aDownload.saver.getSha256Hash();
       sigInfo = aDownload.saver.getSignatureInfo();
       channelRedirects = aDownload.saver.getRedirects();
     } catch (ex) {
@@ -600,26 +562,27 @@ this.DownloadIntegration = {
       redirects: channelRedirects },
       function onComplete(aShouldBlock, aRv, aVerdict) {
         deferred.resolve({
           shouldBlock: aShouldBlock,
           verdict: (aShouldBlock && kVerdictMap[aVerdict]) || "",
         });
       });
     return deferred.promise;
+#endif
   },
 
 #ifdef XP_WIN
   /**
    * Checks whether downloaded files should be marked as coming from
    * Internet Zone.
    *
    * @return true if files should be marked
    */
-  _shouldSaveZoneInformation: function() {
+  _shouldSaveZoneInformation() {
     let key = Cc["@mozilla.org/windows-registry-key;1"]
                 .createInstance(Ci.nsIWindowsRegKey);
     try {
       key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
                "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
                Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
       try {
         return key.readIntValue("SaveZoneInformation") != 1;
@@ -638,106 +601,103 @@ this.DownloadIntegration = {
    *
    * aParam aDownload
    *        The Download object.
    *
    * @return {Promise}
    * @resolves When all the operations completed successfully.
    * @rejects JavaScript exception if any of the operations failed.
    */
-  downloadDone: function(aDownload) {
-    return Task.spawn(function () {
+  downloadDone: Task.async(function* (aDownload) {
 #ifdef XP_WIN
-      // On Windows, we mark any file saved to the NTFS file system as coming
-      // from the Internet security zone unless Group Policy disables the
-      // feature.  We do this by writing to the "Zone.Identifier" Alternate
-      // Data Stream directly, because the Save method of the
-      // IAttachmentExecute interface would trigger operations that may cause
-      // the application to hang, or other performance issues.
-      // The stream created in this way is forward-compatible with all the
-      // current and future versions of Windows.
-      if (this._shouldSaveZoneInformation()) {
-        let zone;
-        try {
-          zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
-        } catch (e) {
-          // Default to Internet Zone if mapUrlToZone failed for
-          // whatever reason.
-          zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
-        }
-        try {
-          // Don't write zone IDs for Local, Intranet, or Trusted sites
-          // to match Windows behavior.
-          if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
-            let streamPath = aDownload.target.path + ":Zone.Identifier";
-            let stream = yield OS.File.open(streamPath, { create: true });
-            try {
-              yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
-            } finally {
-              yield stream.close();
-            }
-          }
-        } catch (ex) {
-          // If writing to the stream fails, we ignore the error and continue.
-          // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
-          // occur when working on a file system that does not support
-          // Alternate Data Streams, like FAT32, thus we don't report this
-          // specific error.
-          if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
-            Cu.reportError(ex);
+    // On Windows, we mark any file saved to the NTFS file system as coming
+    // from the Internet security zone unless Group Policy disables the
+    // feature.  We do this by writing to the "Zone.Identifier" Alternate
+    // Data Stream directly, because the Save method of the
+    // IAttachmentExecute interface would trigger operations that may cause
+    // the application to hang, or other performance issues.
+    // The stream created in this way is forward-compatible with all the
+    // current and future versions of Windows.
+    if (this._shouldSaveZoneInformation()) {
+      let zone;
+      try {
+        zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
+      } catch (e) {
+        // Default to Internet Zone if mapUrlToZone failed for
+        // whatever reason.
+        zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
+      }
+      try {
+        // Don't write zone IDs for Local, Intranet, or Trusted sites
+        // to match Windows behavior.
+        if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
+          let streamPath = aDownload.target.path + ":Zone.Identifier";
+          let stream = yield OS.File.open(streamPath, { create: true });
+          try {
+            yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
+          } finally {
+            yield stream.close();
           }
         }
-      }
-#endif
-
-      // The file with the partially downloaded data has restrictive permissions
-      // that don't allow other users on the system to access it.  Now that the
-      // download is completed, we need to adjust permissions based on whether
-      // this is a permanently downloaded file or a temporary download to be
-      // opened read-only with an external application.
-      try {
-        // The following logic to determine whether this is a temporary download
-        // is due to the fact that "deleteTempFileOnExit" is false on Mac, where
-        // downloads to be opened with external applications are preserved in
-        // the "Downloads" folder like normal downloads.
-        let isTemporaryDownload =
-          aDownload.launchWhenSucceeded && (aDownload.source.isPrivate ||
-          Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit"));
-        // Permanently downloaded files are made accessible by other users on
-        // this system, while temporary downloads are marked as read-only.
-        let options = {};
-        if (isTemporaryDownload) {
-          options.unixMode = 0o400;
-          options.winAttributes = {readOnly: true};
-        } else {
-          options.unixMode = 0o666;
-        }
-        // On Unix, the umask of the process is respected.
-        yield OS.File.setPermissions(aDownload.target.path, options);
       } catch (ex) {
-        // We should report errors with making the permissions less restrictive
-        // or marking the file as read-only on Unix and Mac, but this should not
-        // prevent the download from completing.
-        // The setPermissions API error EPERM is expected to occur when working
-        // on a file system that does not support file permissions, like FAT32,
-        // thus we don't report this error.
-        if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
+        // If writing to the stream fails, we ignore the error and continue.
+        // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
+        // occur when working on a file system that does not support
+        // Alternate Data Streams, like FAT32, thus we don't report this
+        // specific error.
+        if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
           Cu.reportError(ex);
         }
       }
+    }
+#endif
 
-      gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
-                                     new FileUtils.File(aDownload.target.path),
-                                     aDownload.contentType,
-                                     aDownload.source.isPrivate);
-      this.downloadDoneCalled = true;
-    }.bind(this));
-  },
+    // The file with the partially downloaded data has restrictive permissions
+    // that don't allow other users on the system to access it.  Now that the
+    // download is completed, we need to adjust permissions based on whether
+    // this is a permanently downloaded file or a temporary download to be
+    // opened read-only with an external application.
+    try {
+      // The following logic to determine whether this is a temporary download
+      // is due to the fact that "deleteTempFileOnExit" is false on Mac, where
+      // downloads to be opened with external applications are preserved in
+      // the "Downloads" folder like normal downloads.
+      let isTemporaryDownload =
+        aDownload.launchWhenSucceeded && (aDownload.source.isPrivate ||
+        Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit"));
+      // Permanently downloaded files are made accessible by other users on
+      // this system, while temporary downloads are marked as read-only.
+      let options = {};
+      if (isTemporaryDownload) {
+        options.unixMode = 0o400;
+        options.winAttributes = {readOnly: true};
+      } else {
+        options.unixMode = 0o666;
+      }
+      // On Unix, the umask of the process is respected.
+      yield OS.File.setPermissions(aDownload.target.path, options);
+    } catch (ex) {
+      // We should report errors with making the permissions less restrictive
+      // or marking the file as read-only on Unix and Mac, but this should not
+      // prevent the download from completing.
+      // The setPermissions API error EPERM is expected to occur when working
+      // on a file system that does not support file permissions, like FAT32,
+      // thus we don't report this error.
+      if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
+        Cu.reportError(ex);
+      }
+    }
 
-  /*
+    gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
+                                   new FileUtils.File(aDownload.target.path),
+                                   aDownload.contentType,
+                                   aDownload.source.isPrivate);
+  }),
+
+  /**
    * Launches a file represented by the target of a download. This can
    * open the file with the default application for the target MIME type
    * or file extension, or with a custom application if
    * aDownload.launcherPath is set.
    *
    * @param    aDownload
    *           A Download object that contains the necessary information
    *           to launch the file. The relevant properties are: the target
@@ -747,231 +707,199 @@ this.DownloadIntegration = {
    * @return {Promise}
    * @resolves When the instruction to launch the file has been
    *           successfully given to the operating system. Note that
    *           the OS might still take a while until the file is actually
    *           launched.
    * @rejects  JavaScript exception if there was an error trying to launch
    *           the file.
    */
-  launchDownload: function (aDownload) {
-    let deferred = Task.spawn(function DI_launchDownload_task() {
-      let file = new FileUtils.File(aDownload.target.path);
+  launchDownload: Task.async(function* (aDownload) {
+    let file = new FileUtils.File(aDownload.target.path);
 
 #ifndef XP_WIN
-      // Ask for confirmation if the file is executable, except on Windows where
-      // the operating system will show the prompt based on the security zone.
-      // We do this here, instead of letting the caller handle the prompt
-      // separately in the user interface layer, for two reasons.  The first is
-      // because of its security nature, so that add-ons cannot forget to do
-      // this check.  The second is that the system-level security prompt would
-      // be displayed at launch time in any case.
-      if (file.isExecutable() && !this.dontOpenFileAndFolder) {
-        // We don't anchor the prompt to a specific window intentionally, not
-        // only because this is the same behavior as the system-level prompt,
-        // but also because the most recently active window is the right choice
-        // in basically all cases.
-        let shouldLaunch = yield DownloadUIHelper.getPrompter()
-                                   .confirmLaunchExecutable(file.path);
-        if (!shouldLaunch) {
-          return;
-        }
-      }
+    // Ask for confirmation if the file is executable, except on Windows where
+    // the operating system will show the prompt based on the security zone.
+    // We do this here, instead of letting the caller handle the prompt
+    // separately in the user interface layer, for two reasons.  The first is
+    // because of its security nature, so that add-ons cannot forget to do
+    // this check.  The second is that the system-level security prompt would
+    // be displayed at launch time in any case.
+    if (file.isExecutable() &&
+        !(yield this.confirmLaunchExecutable(file.path))) {
+      return;
+    }
 #endif
 
-      // In case of a double extension, like ".tar.gz", we only
-      // consider the last one, because the MIME service cannot
-      // handle multiple extensions.
-      let fileExtension = null, mimeInfo = null;
-      let match = file.leafName.match(/\.([^.]+)$/);
-      if (match) {
-        fileExtension = match[1];
+    // In case of a double extension, like ".tar.gz", we only
+    // consider the last one, because the MIME service cannot
+    // handle multiple extensions.
+    let fileExtension = null, mimeInfo = null;
+    let match = file.leafName.match(/\.([^.]+)$/);
+    if (match) {
+      fileExtension = match[1];
+    }
+
+    try {
+      // The MIME service might throw if contentType == "" and it can't find
+      // a MIME type for the given extension, so we'll treat this case as
+      // an unknown mimetype.
+      mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
+                                                      fileExtension);
+    } catch (e) { }
+
+    if (aDownload.launcherPath) {
+      if (!mimeInfo) {
+        // This should not happen on normal circumstances because launcherPath
+        // is only set when we had an instance of nsIMIMEInfo to retrieve
+        // the custom application chosen by the user.
+        throw new Error(
+          "Unable to create nsIMIMEInfo to launch a custom application");
       }
 
+      // Custom application chosen
+      let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+                              .createInstance(Ci.nsILocalHandlerApp);
+      localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
+
+      mimeInfo.preferredApplicationHandler = localHandlerApp;
+      mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+
+      this.launchFile(file, mimeInfo);
+      return;
+    }
+
+    // No custom application chosen, let's launch the file with the default
+    // handler. First, let's try to launch it through the MIME service.
+    if (mimeInfo) {
+      mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
+
       try {
-        // The MIME service might throw if contentType == "" and it can't find
-        // a MIME type for the given extension, so we'll treat this case as
-        // an unknown mimetype.
-        mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
-                                                        fileExtension);
-      } catch (e) { }
-
-      if (aDownload.launcherPath) {
-        if (!mimeInfo) {
-          // This should not happen on normal circumstances because launcherPath
-          // is only set when we had an instance of nsIMIMEInfo to retrieve
-          // the custom application chosen by the user.
-          throw new Error(
-            "Unable to create nsIMIMEInfo to launch a custom application");
-        }
-
-        // Custom application chosen
-        let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
-                                .createInstance(Ci.nsILocalHandlerApp);
-        localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
-
-        mimeInfo.preferredApplicationHandler = localHandlerApp;
-        mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
-
-        // In test mode, allow the test to verify the nsIMIMEInfo instance.
-        if (this.dontOpenFileAndFolder) {
-          throw new Task.Result(mimeInfo);
-        }
-
-        mimeInfo.launchWithFile(file);
-        return;
-      }
-
-      // No custom application chosen, let's launch the file with the default
-      // handler.  In test mode, we indicate this with a null value.
-      if (this.dontOpenFileAndFolder) {
-        throw new Task.Result(null);
-      }
-
-      // First let's try to launch it through the MIME service application
-      // handler
-      if (mimeInfo) {
-        mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
-
-        try {
-          mimeInfo.launchWithFile(file);
-          return;
-        } catch (ex) { }
-      }
-
-      // If it didn't work or if there was no MIME info available,
-      // let's try to directly launch the file.
-      try {
-        file.launch();
+        this.launchFile(file, mimeInfo);
         return;
       } catch (ex) { }
-
-      // If our previous attempts failed, try sending it through
-      // the system's external "file:" URL handler.
-      gExternalProtocolService.loadUrl(NetUtil.newURI(file));
-      yield undefined;
-    }.bind(this));
-
-    if (this.dontOpenFileAndFolder) {
-      deferred.then((value) => { this._deferTestOpenFile.resolve(value); },
-                    (error) => { this._deferTestOpenFile.reject(error); });
     }
 
-    return deferred;
+    // If it didn't work or if there was no MIME info available,
+    // let's try to directly launch the file.
+    try {
+      this.launchFile(file);
+      return;
+    } catch (ex) { }
+
+    // If our previous attempts failed, try sending it through
+    // the system's external "file:" URL handler.
+    gExternalProtocolService.loadUrl(NetUtil.newURI(file));
+  }),
+
+  /**
+   * Asks for confirmation for launching the specified executable file. This
+   * can be overridden by regression tests to avoid the interactive prompt.
+   */
+  confirmLaunchExecutable: Task.async(function* (path) {
+    // We don't anchor the prompt to a specific window intentionally, not
+    // only because this is the same behavior as the system-level prompt,
+    // but also because the most recently active window is the right choice
+    // in basically all cases.
+    return yield DownloadUIHelper.getPrompter().confirmLaunchExecutable(path);
+  }),
+
+  /**
+   * Launches the specified file, unless overridden by regression tests.
+   */
+  launchFile(file, mimeInfo) {
+    if (mimeInfo) {
+      mimeInfo.launchWithFile(file);
+    } else {
+      file.launch();
+    }
   },
 
-  /*
+  /**
    * Shows the containing folder of a file.
    *
-   * @param    aFilePath
-   *           The path to the file.
+   * @param aFilePath
+   *        The path to the file.
    *
    * @return {Promise}
    * @resolves When the instruction to open the containing folder has been
    *           successfully given to the operating system. Note that
    *           the OS might still take a while until the folder is actually
    *           opened.
    * @rejects  JavaScript exception if there was an error trying to open
    *           the containing folder.
    */
-  showContainingDirectory: function (aFilePath) {
-    let deferred = Task.spawn(function DI_showContainingDirectory_task() {
-      let file = new FileUtils.File(aFilePath);
-
-      if (this.dontOpenFileAndFolder) {
-        return;
-      }
-
-      try {
-        // Show the directory containing the file and select the file.
-        file.reveal();
-        return;
-      } catch (ex) { }
-
-      // If reveal fails for some reason (e.g., it's not implemented on unix
-      // or the file doesn't exist), try using the parent if we have it.
-      let parent = file.parent;
-      if (!parent) {
-        throw new Error(
-          "Unexpected reference to a top-level directory instead of a file");
-      }
+  showContainingDirectory: Task.async(function* (aFilePath) {
+    let file = new FileUtils.File(aFilePath);
 
-      try {
-        // Open the parent directory to show where the file should be.
-        parent.launch();
-        return;
-      } catch (ex) { }
+    try {
+      // Show the directory containing the file and select the file.
+      file.reveal();
+      return;
+    } catch (ex) { }
 
-      // If launch also fails (probably because it's not implemented), let
-      // the OS handler try to open the parent.
-      gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
-      yield undefined;
-    }.bind(this));
-
-    if (this.dontOpenFileAndFolder) {
-      deferred.then((value) => { this._deferTestShowDir.resolve("success"); },
-                    (error) => {
-                      // Ensure that _deferTestShowDir has at least one consumer
-                      // for the error, otherwise the error will be reported as
-                      // uncaught.
-                      this._deferTestShowDir.promise.then(null, function() {});
-                      this._deferTestShowDir.reject(error);
-                    });
+    // If reveal fails for some reason (e.g., it's not implemented on unix
+    // or the file doesn't exist), try using the parent if we have it.
+    let parent = file.parent;
+    if (!parent) {
+      throw new Error(
+        "Unexpected reference to a top-level directory instead of a file");
     }
 
-    return deferred;
-  },
+    try {
+      // Open the parent directory to show where the file should be.
+      parent.launch();
+      return;
+    } catch (ex) { }
+
+    // If launch also fails (probably because it's not implemented), let
+    // the OS handler try to open the parent.
+    gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
+  }),
 
   /**
    * Calls the directory service, create a downloads directory and returns an
    * nsIFile for the downloads directory.
    *
    * @return {Promise}
    * @resolves The directory string path.
    */
-  _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
+  _createDownloadsDirectory(aName) {
     // We read the name of the directory from the list of translated strings
     // that is kept by the UI helper module, even if this string is not strictly
     // displayed in the user interface.
     let directoryPath = OS.Path.join(this._getDirectory(aName),
                                      DownloadUIHelper.strings.downloadsFolder);
 
     // Create the Downloads folder and ignore if it already exists.
-    return OS.File.makeDir(directoryPath, { ignoreExisting: true }).
-             then(function() {
-               return directoryPath;
-             });
+    return OS.File.makeDir(directoryPath, { ignoreExisting: true })
+                  .then(() => directoryPath);
   },
 
   /**
-   * Calls the directory service and returns an nsIFile for the requested
-   * location name.
-   *
-   * @return The directory string path.
+   * Returns the string path for the given directory service location name. This
+   * can be overridden by regression tests to return the path of the system
+   * temporary directory in all cases.
    */
-  _getDirectory: function DI_getDirectory(aName) {
-    return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path;
+  _getDirectory(name) {
+    return Services.dirsvc.get(name, Ci.nsIFile).path;
   },
 
   /**
    * Register the downloads interruption observers.
    *
    * @param aList
    *        The public or private downloads list.
    * @param aIsPrivate
    *        True if the list is private, false otherwise.
    *
    * @return {Promise}
    * @resolves When the views and observers are added.
    */
-  addListObservers: function DI_addListObservers(aList, aIsPrivate) {
-    if (this.dontLoadObservers) {
-      return Promise.resolve();
-    }
-
+  addListObservers(aList, aIsPrivate) {
     DownloadObserver.registerView(aList, aIsPrivate);
     if (!DownloadObserver.observersAdded) {
       DownloadObserver.observersAdded = true;
       for (let topic of kObserverTopics) {
         Services.obs.addObserver(DownloadObserver, topic, false);
       }
     }
     return Promise.resolve();
@@ -979,17 +907,17 @@ this.DownloadIntegration = {
 
   /**
    * Force a save on _store if it exists. Used to ensure downloads do not
    * persist after being sanitized on Android.
    *
    * @return {Promise}
    * @resolves When _store.save() completes.
    */
-  forceSave: function DI_forceSave() {
+  forceSave() {
     if (this._store) {
       return this._store.save();
     }
     return Promise.resolve();
   },
 
   /**
    * Checks if we have already imported (or attempted to import)
@@ -1096,18 +1024,18 @@ this.DownloadObserver = {
    */
   _confirmCancelDownloads: function DO_confirmCancelDownload(
     aCancel, aDownloadsCount, aPrompter, aPromptType) {
     // If user has already dismissed the request, then do nothing.
     if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
       return;
     }
     // Handle test mode
-    if (DownloadIntegration.testMode) {
-      DownloadIntegration.testPromptDownloads = aDownloadsCount;
+    if (gCombinedDownloadIntegration._testPromptDownloads) {
+      gCombinedDownloadIntegration._testPromptDownloads = aDownloadsCount;
       return;
     }
 
     aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType);
   },
 
   /**
    * Resume all downloads that were paused when going offline, used when waking
@@ -1139,30 +1067,31 @@ this.DownloadObserver = {
         this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE);
         break;
       case "last-pb-context-exiting":
         downloadsCount = this._privateInProgressDownloads.size;
         this._confirmCancelDownloads(aSubject, downloadsCount, p,
                                      p.ON_LEAVE_PRIVATE_BROWSING);
         break;
       case "last-pb-context-exited":
-        let deferred = Task.spawn(function() {
+        let promise = Task.spawn(function() {
           let list = yield Downloads.getList(Downloads.PRIVATE);
           let downloads = yield list.getAll();
 
           // We can remove the downloads and finalize them in parallel.
           for (let download of downloads) {
             list.remove(download).then(null, Cu.reportError);
             download.finalize(true).then(null, Cu.reportError);
           }
         });
         // Handle test mode
-        if (DownloadIntegration.testMode) {
-          deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); },
-                        (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); });
+        if (gCombinedDownloadIntegration._testResolveClearPrivateList) {
+          gCombinedDownloadIntegration._testResolveClearPrivateList(promise);
+        } else {
+          promise.catch(ex => Cu.reportError(ex));
         }
         break;
       case "sleep_notification":
       case "suspend_process_notification":
       case "network:offline-about-to-go-offline":
         for (let download of this._publicInProgressDownloads) {
           download.cancel();
           this._canceledOfflineDownloads.add(download);
@@ -1344,27 +1273,27 @@ this.DownloadAutoSaveView.prototype = {
     this._writer.arm();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// DownloadList view
 
   onDownloadAdded: function (aDownload)
   {
-    if (DownloadIntegration.shouldPersistDownload(aDownload)) {
+    if (gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
       this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
       if (this._initialized) {
         this.saveSoon();
       }
     }
   },
 
   onDownloadChanged: function (aDownload)
   {
-    if (!DownloadIntegration.shouldPersistDownload(aDownload)) {
+    if (!gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
       if (this._downloadsMap.has(aDownload)) {
         this._downloadsMap.delete(aDownload);
         this.saveSoon();
       }
       return;
     }
 
     let hash = aDownload.getSerializationHash();
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -17,34 +17,36 @@ this.EXPORTED_SYMBOLS = [
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/Integration.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/DownloadCore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadCombinedList",
                                   "resource://gre/modules/DownloadList.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
-                                  "resource://gre/modules/DownloadIntegration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
                                   "resource://gre/modules/DownloadList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadSummary",
                                   "resource://gre/modules/DownloadList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+            "resource://gre/modules/DownloadIntegration.jsm");
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Downloads
 
 /**
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides the only entry point to get references to back-end objects.
  */
 this.Downloads = {
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -125,16 +125,55 @@ function promisePartFileReady(aDownload)
  */
 var promiseVerifyTarget = Task.async(function* (downloadTarget,
                                                 expectedContents) {
   yield promiseVerifyContents(downloadTarget.path, expectedContents);
   do_check_true(downloadTarget.exists);
   do_check_eq(downloadTarget.size, expectedContents.length);
 });
 
+/**
+ * Waits for an attempt to launch a file, and returns the nsIMIMEInfo used for
+ * the launch, or null if the file was launched with the default handler.
+ */
+function waitForFileLaunched() {
+  return new Promise(resolve => {
+    let waitFn = base => ({
+      launchFile(file, mimeInfo) {
+        Integration.downloads.unregister(waitFn);
+        if (!mimeInfo ||
+            mimeInfo.preferredAction == Ci.nsIMIMEInfo.useSystemDefault) {
+          resolve(null);
+        } else {
+          resolve(mimeInfo);
+        }
+        return Promise.resolve();
+      },
+    });
+    Integration.downloads.register(waitFn);
+  });
+}
+
+/**
+ * Waits for an attempt to show the directory where a file is located, and
+ * returns the path of the file.
+ */
+function waitForDirectoryShown() {
+  return new Promise(resolve => {
+    let waitFn = base => ({
+      showContainingDirectory(path) {
+        Integration.downloads.unregister(waitFn);
+        resolve(path);
+        return Promise.resolve();
+      },
+    });
+    Integration.downloads.register(waitFn);
+  });
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 /**
  * Executes a download and checks its basic properties after construction.
  * The download is started by constructing the simplest Download object with
  * the "copy" saver, or using the legacy nsITransfer interface.
  */
@@ -1570,21 +1609,25 @@ add_task(function* test_cancel_midway_re
   yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
 });
 
 /**
  * Download with parental controls enabled.
  */
 add_task(function* test_blocked_parental_controls()
 {
+  let blockFn = base => ({
+    shouldBlockForParentalControls: () => Promise.resolve(true),
+  });
+
+  Integration.downloads.register(blockFn);
   function cleanup() {
-    DownloadIntegration.shouldBlockInTest = false;
+    Integration.downloads.unregister(blockFn);
   }
   do_register_cleanup(cleanup);
-  DownloadIntegration.shouldBlockInTest = true;
 
   let download;
   try {
     if (!gUseLegacySaver) {
       // When testing DownloadCopySaver, we want to check that the promise
       // returned by the "start" method is rejected.
       download = yield promiseNewDownload();
       yield download.start();
@@ -1640,21 +1683,25 @@ add_task(function* test_blocked_parental
   do_check_false(yield OS.File.exists(download.target.path));
 });
 
 /**
  * Download with runtime permissions
  */
 add_task(function* test_blocked_runtime_permissions()
 {
+  let blockFn = base => ({
+    shouldBlockForRuntimePermissions: () => Promise.resolve(true),
+  });
+
+  Integration.downloads.register(blockFn);
   function cleanup() {
-    DownloadIntegration.shouldBlockInTestForRuntimePermissions = false;
+    Integration.downloads.unregister(blockFn);
   }
   do_register_cleanup(cleanup);
-  DownloadIntegration.shouldBlockInTestForRuntimePermissions = true;
 
   let download;
   try {
     if (!gUseLegacySaver) {
       // When testing DownloadCopySaver, we want to check that the promise
       // returned by the "start" method is rejected.
       download = yield promiseNewDownload();
       yield download.start();
@@ -1704,33 +1751,34 @@ add_task(function* test_getSha256Hash()
  *           keepPartialData: bool,
  *           keepBlockedData: bool,
  *        }
  * @return {Promise}
  * @resolves The reputation blocked download.
  * @rejects JavaScript exception.
  */
 var promiseBlockedDownload = Task.async(function* (options) {
+  let blockFn = base => ({
+    shouldBlockForReputationCheck: () => Promise.resolve({
+      shouldBlock: true,
+      verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+    }),
+    shouldKeepBlockedData: () => Promise.resolve(options.keepBlockedData),
+  });
+
+  Integration.downloads.register(blockFn);
   function cleanup() {
-    DownloadIntegration.shouldBlockInTestForApplicationReputation = false;
-    DownloadIntegration.verdictInTestForApplicationReputation = "";
-    DownloadIntegration.shouldKeepBlockedDataInTest = false;
+    Integration.downloads.unregister(blockFn);
   }
   do_register_cleanup(cleanup);
 
-  let {keepPartialData, keepBlockedData} = options;
-  DownloadIntegration.shouldBlockInTestForApplicationReputation = true;
-  DownloadIntegration.verdictInTestForApplicationReputation =
-                                        Downloads.Error.BLOCK_VERDICT_UNCOMMON;
-  DownloadIntegration.shouldKeepBlockedDataInTest = keepBlockedData;
-
   let download;
 
   try {
-    if (keepPartialData) {
+    if (options.keepPartialData) {
       download = yield promiseStartDownload_tryToKeepPartialData();
       continueResponses();
     } else if (gUseLegacySaver) {
       download = yield promiseStartLegacyDownload();
     } else {
       download = yield promiseNewDownload();
       yield download.start();
       do_throw("The download should have blocked.");
@@ -1939,27 +1987,28 @@ add_task(function* test_blocked_applicat
   do_check_false(download.target.exists);
   do_check_eq(download.target.size, 0);
 });
 
 /**
  * download.showContainingDirectory() action
  */
 add_task(function* test_showContainingDirectory() {
-  DownloadIntegration._deferTestShowDir = Promise.defer();
-
   let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
 
   let download = yield Downloads.createDownload({
     source: { url: httpUrl("source.txt") },
     target: ""
   });
 
+  let promiseDirectoryShown = waitForDirectoryShown();
+  yield download.showContainingDirectory();
+  let path = yield promiseDirectoryShown;
   try {
-    yield download.showContainingDirectory();
+    new FileUtils.File(path);
     do_throw("Should have failed because of an invalid path.");
   } catch (ex) {
     if (!(ex instanceof Components.Exception)) {
       throw ex;
     }
     // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
     // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
     let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
@@ -1967,21 +2016,19 @@ add_task(function* test_showContainingDi
     do_check_true(validResult);
   }
 
   download = yield Downloads.createDownload({
     source: { url: httpUrl("source.txt") },
     target: targetPath
   });
 
-
-  DownloadIntegration._deferTestShowDir = Promise.defer();
+  promiseDirectoryShown = waitForDirectoryShown();
   download.showContainingDirectory();
-  let result = yield DownloadIntegration._deferTestShowDir.promise;
-  do_check_eq(result, "success");
+  yield promiseDirectoryShown;
 });
 
 /**
  * download.launch() action
  */
 add_task(function* test_launch() {
   let customLauncher = getTempFile("app-launcher");
 
@@ -2014,19 +2061,19 @@ add_task(function* test_launch() {
                                          httpUrl("source.txt"),
                                          { launcherPath: launcherPath,
                                            launchWhenSucceeded: true });
       yield promiseDownloadStopped(download);
     }
 
     do_check_true(download.launchWhenSucceeded);
 
-    DownloadIntegration._deferTestOpenFile = Promise.defer();
+    let promiseFileLaunched = waitForFileLaunched();
     download.launch();
-    let result = yield DownloadIntegration._deferTestOpenFile.promise;
+    let result = yield promiseFileLaunched;
 
     // Verify that the results match the test case.
     if (!launcherPath) {
       // This indicates that the default handler has been chosen.
       do_check_true(result === null);
     } else {
       // Check the nsIMIMEInfo instance that would have been used for launching.
       do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
@@ -2042,21 +2089,33 @@ add_task(function* test_launch() {
  */
 add_task(function* test_launcherPath_invalid() {
   let download = yield Downloads.createDownload({
     source: { url: httpUrl("source.txt") },
     target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
     launcherPath: " "
   });
 
-  DownloadIntegration._deferTestOpenFile = Promise.defer();
+  let promiseDownloadLaunched = new Promise(resolve => {
+    let waitFn = base => ({
+      __proto__: base,
+      launchDownload() {
+        Integration.downloads.unregister(waitFn);
+        let superPromise = super.launchDownload(...arguments);
+        resolve(superPromise);
+        return superPromise;
+      },
+    });
+    Integration.downloads.register(waitFn);
+  });
+
   yield download.start();
   try {
     download.launch();
-    result = yield DownloadIntegration._deferTestOpenFile.promise;
+    yield promiseDownloadLaunched;
     do_throw("Can't launch file with invalid custom launcher")
   } catch (ex) {
     if (!(ex instanceof Components.Exception)) {
       throw ex;
     }
     // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
     // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
     let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
@@ -2069,17 +2128,17 @@ add_task(function* test_launcherPath_inv
  * Tests that download.launch() is automatically called after
  * the download finishes if download.launchWhenSucceeded = true
  */
 add_task(function* test_launchWhenSucceeded() {
   let customLauncher = getTempFile("app-launcher");
 
   // Test both with and without setting a custom application.
   for (let launcherPath of [null, customLauncher.path]) {
-    DownloadIntegration._deferTestOpenFile = Promise.defer();
+    let promiseFileLaunched = waitForFileLaunched();
 
     if (!gUseLegacySaver) {
       let download = yield Downloads.createDownload({
         source: httpUrl("source.txt"),
         target: getTempFile(TEST_TARGET_FILE_NAME).path,
         launchWhenSucceeded: true,
         launcherPath: launcherPath,
       });
@@ -2087,17 +2146,17 @@ add_task(function* test_launchWhenSuccee
     } else {
       let download = yield promiseStartLegacyDownload(
                                              httpUrl("source.txt"),
                                              { launcherPath: launcherPath,
                                                launchWhenSucceeded: true });
       yield promiseDownloadStopped(download);
     }
 
-    let result = yield DownloadIntegration._deferTestOpenFile.promise;
+    let result = yield promiseFileLaunched;
 
     // Verify that the results match the test case.
     if (!launcherPath) {
       // This indicates that the default handler has been chosen.
       do_check_true(result === null);
     } else {
       // Check the nsIMIMEInfo instance that would have been used for launching.
       do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
@@ -2156,27 +2215,39 @@ add_task(function* test_platform_integra
     observe: function(subject, topic, data) {
       do_check_eq(topic, "download-watcher-notify");
       do_check_eq(data, "modified");
       downloadWatcherNotified = true;
     }
   }
   Services.obs.addObserver(observer, "download-watcher-notify", false);
   Services.prefs.setBoolPref("device.storage.enabled", true);
+  let downloadDoneCalled = false;
+  let monitorFn = base => ({
+    __proto__: base,
+    downloadDone() {
+      return super.downloadDone(...arguments).then(() => {
+        downloadDoneCalled = true;
+      });
+    },
+  });
+  Integration.downloads.register(monitorFn);
+  DownloadIntegration.allowDirectories = true;
   function cleanup() {
     for (let file of downloadFiles) {
       file.remove(true);
     }
     Services.obs.removeObserver(observer, "download-watcher-notify");
     Services.prefs.setBoolPref("device.storage.enabled", oldDeviceStorageEnabled);
+    Integration.downloads.unregister(monitorFn);
+    DownloadIntegration.allowDirectories = false;
   }
-  do_register_cleanup(cleanup);
 
   for (let isPrivate of [false, true]) {
-    DownloadIntegration.downloadDoneCalled = false;
+    downloadDoneCalled = false;
 
     // Some platform specific operations only operate on files outside the
     // temporary directory or in the Downloads directory (such as setting
     // the Windows searchable attribute, and the Mac Downloads icon bouncing),
     // so use the system Downloads directory for the target file.
     let targetFilePath = yield DownloadIntegration.getSystemDownloadsDirectory();
     targetFilePath = OS.Path.join(targetFilePath,
                                   "test" + (Math.floor(Math.random() * 1000000)));
@@ -2194,25 +2265,27 @@ add_task(function* test_platform_integra
         target: targetFile,
       });
       download.start().catch(() => {});
     }
 
     // Wait for the whenSucceeded promise to be resolved first.
     // downloadDone should be called before the whenSucceeded promise is resolved.
     yield download.whenSucceeded().then(function () {
-      do_check_true(DownloadIntegration.downloadDoneCalled);
+      do_check_true(downloadDoneCalled);
       do_check_true(downloadWatcherNotified);
     });
 
     // Then, wait for the promise returned by "start" to be resolved.
     yield promiseDownloadStopped(download);
 
     yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
   }
+
+  cleanup();
 });
 
 /**
  * Checks that downloads are added to browsing history when they start.
  */
 add_task(function* test_history()
 {
   mustInterruptResponses();
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -12,22 +12,21 @@
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
 
+Cu.import("resource://gre/modules/Integration.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                   "resource://gre/modules/DownloadPaths.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
-                                  "resource://gre/modules/DownloadIntegration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
@@ -45,16 +44,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
                                   "resource://testing-common/MockRegistrar.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
 
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+            "resource://gre/modules/DownloadIntegration.jsm");
+
 const ServerSocket = Components.Constructor(
                                 "@mozilla.org/network/server-socket;1",
                                 "nsIServerSocket",
                                 "init");
 const BinaryOutputStream = Components.Constructor(
                                       "@mozilla.org/binaryoutputstream;1",
                                       "nsIBinaryOutputStream",
                                       "setOutputStream")
@@ -782,33 +784,49 @@ add_task(function test_common_initialize
 
   // This URL will emulate being blocked by Windows Parental controls
   gHttpServer.registerPathHandler("/parentalblocked.zip",
     function (aRequest, aResponse) {
       aResponse.setStatusLine(aRequest.httpVersion, 450,
                               "Blocked by Windows Parental Controls");
     });
 
-  // Disable integration with the host application requiring profile access.
-  DownloadIntegration.dontLoadList = true;
-  DownloadIntegration.dontLoadObservers = true;
-  // Disable the parental controls checking.
-  DownloadIntegration.dontCheckParentalControls = true;
-  // Disable application reputation checks.
-  DownloadIntegration.dontCheckApplicationReputation = true;
-  // Disable the calls to the OS to launch files and open containing folders
-  DownloadIntegration.dontOpenFileAndFolder = true;
-  DownloadIntegration._deferTestOpenFile = Promise.defer();
-  DownloadIntegration._deferTestShowDir = Promise.defer();
-  // Disable checking runtime permissions.
-  DownloadIntegration.dontCheckRuntimePermissions = true;
-
-  // Avoid leaking uncaught promise errors
-  DownloadIntegration._deferTestOpenFile.promise.then(null, () => undefined);
-  DownloadIntegration._deferTestShowDir.promise.then(null, () => undefined);
+  // During unit tests, most of the functions that require profile access or
+  // operating system features will be disabled. Individual tests may override
+  // them again to check for specific behaviors.
+  Integration.downloads.register(base => ({
+    __proto__: base,
+    loadPublicDownloadListFromStore: () => Promise.resolve(),
+    shouldKeepBlockedData: () => Promise.resolve(false),
+    shouldBlockForParentalControls: () => Promise.resolve(false),
+    shouldBlockForRuntimePermissions: () => Promise.resolve(false),
+    shouldBlockForReputationCheck: () => Promise.resolve({
+      shouldBlock: false,
+      verdict: "",
+    }),
+    confirmLaunchExecutable: () => Promise.resolve(),
+    launchFile: () => Promise.resolve(),
+    showContainingDirectory: () => Promise.resolve(),
+    // This flag allows re-enabling the default observers during their tests.
+    allowObservers: false,
+    addListObservers() {
+      return this.allowObservers ? super.addListObservers(...arguments)
+                                 : Promise.resolve();
+    },
+    // This flag allows re-enabling the download directory logic for its tests.
+    _allowDirectories: false,
+    set allowDirectories(value) {
+      this._allowDirectories = value;
+      // We have to invalidate the previously computed directory path.
+      this._downloadsDirectory = null;
+    },
+    _getDirectory(name) {
+      return super._getDirectory(this._allowDirectories ? name : "TmpD");
+    },
+  }));
 
   // Get a reference to nsIComponentRegistrar, and ensure that is is freed
   // before the XPCOM shutdown.
   let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
   do_register_cleanup(() => registrar = null);
 
   // Make sure that downloads started using nsIExternalHelperAppService are
   // saved to disk without asking for a destination interactively.
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
@@ -6,84 +6,77 @@
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 /**
- * Enable test mode for the _confirmCancelDownloads method to return
- * the number of downloads instead of showing the prompt to cancel or not.
- */
-function enableObserversTestMode() {
-  DownloadIntegration.testMode = true;
-  DownloadIntegration.dontLoadObservers = false;
-  function cleanup() {
-    DownloadIntegration.testMode = false;
-    DownloadIntegration.dontLoadObservers = true;
-  }
-  do_register_cleanup(cleanup);
-}
-
-/**
  * Notifies the prompt observers and verify the expected downloads count.
  *
  * @param aIsPrivate
  *        Flag to know is test private observers.
  * @param aExpectedCount
  *        the expected downloads count for quit and offline observers.
  * @param aExpectedPBCount
  *        the expected downloads count for private browsing observer.
  */
 function notifyPromptObservers(aIsPrivate, aExpectedCount, aExpectedPBCount) {
   let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
                    createInstance(Ci.nsISupportsPRBool);
 
   // Notify quit application requested observer.
-  DownloadIntegration.testPromptDownloads = -1;
+  DownloadIntegration._testPromptDownloads = -1;
   Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
-  do_check_eq(DownloadIntegration.testPromptDownloads, aExpectedCount);
+  do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
 
   // Notify offline requested observer.
-  DownloadIntegration.testPromptDownloads = -1;
+  DownloadIntegration._testPromptDownloads = -1;
   Services.obs.notifyObservers(cancelQuit, "offline-requested", null);
-  do_check_eq(DownloadIntegration.testPromptDownloads, aExpectedCount);
+  do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
 
   if (aIsPrivate) {
     // Notify last private browsing requested observer.
-    DownloadIntegration.testPromptDownloads = -1;
+    DownloadIntegration._testPromptDownloads = -1;
     Services.obs.notifyObservers(cancelQuit, "last-pb-context-exiting", null);
-    do_check_eq(DownloadIntegration.testPromptDownloads, aExpectedPBCount);
+    do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedPBCount);
   }
+
+  delete DownloadIntegration._testPromptDownloads;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
+/**
+ * Allows re-enabling the real download directory logic during one test.
+ */
+function allowDirectoriesInTest() {
+  DownloadIntegration.allowDirectories = true;
+  function cleanup() {
+    DownloadIntegration.allowDirectories = false;
+  }
+  do_register_cleanup(cleanup);
+  return cleanup;
+}
+
 XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
   return Services.strings.
     createBundle("chrome://mozapps/locale/downloads/downloads.properties");
 });
 
 /**
- * Tests that the getSystemDownloadsDirectory returns a valid download
- * directory string path.
+ * Tests that getSystemDownloadsDirectory returns an existing directory or
+ * creates a new directory depending on the platform. Instead of the real
+ * directory, this test is executed in the temporary directory so we can safely
+ * delete the created folder to check whether it is created again.
  */
-add_task(function* test_getSystemDownloadsDirectory()
+add_task(function* test_getSystemDownloadsDirectory_exists_or_creates()
 {
-  // Enable test mode for the getSystemDownloadsDirectory method to return
-  // temp directory instead so we can check whether the desired directory
-  // is created or not.
-  DownloadIntegration.testMode = true;
-  function cleanup() {
-    DownloadIntegration.testMode = false;
-  }
-  do_register_cleanup(cleanup);
-
   let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
   let downloadDir;
 
   // OSX / Linux / Windows but not XP/2k
   if (Services.appinfo.OS == "Darwin" ||
       Services.appinfo.OS == "Linux" ||
       (Services.appinfo.OS == "WINNT" &&
        parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
@@ -102,36 +95,49 @@ add_task(function* test_getSystemDownloa
     downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
     do_check_eq(downloadDir, targetPath);
     do_check_true(yield OS.File.exists(downloadDir));
 
     let info = yield OS.File.stat(downloadDir);
     do_check_true(info.isDir);
     yield OS.File.removeEmptyDir(targetPath);
   }
+});
 
-  let downloadDirBefore = yield DownloadIntegration.getSystemDownloadsDirectory();
+/**
+ * Tests that the real directory returned by getSystemDownloadsDirectory is not
+ * the one that is used during unit tests. Since this is the actual downloads
+ * directory of the operating system, we don't try to delete it afterwards.
+ */
+add_task(function* test_getSystemDownloadsDirectory_real()
+{
+  let fakeDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+
+  let cleanup = allowDirectoriesInTest();
+  let realDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
   cleanup();
-  let downloadDirAfter = yield DownloadIntegration.getSystemDownloadsDirectory();
-  do_check_neq(downloadDirBefore, downloadDirAfter);
+
+  do_check_neq(fakeDownloadDir, realDownloadDir);
 });
 
 /**
  * Tests that the getPreferredDownloadsDirectory returns a valid download
  * directory string path.
  */
 add_task(function* test_getPreferredDownloadsDirectory()
 {
+  let cleanupDirectories = allowDirectoriesInTest();
+
   let folderListPrefName = "browser.download.folderList";
   let dirPrefName = "browser.download.dir";
-  function cleanup() {
+  function cleanupPrefs() {
     Services.prefs.clearUserPref(folderListPrefName);
     Services.prefs.clearUserPref(dirPrefName);
   }
-  do_register_cleanup(cleanup);
+  do_register_cleanup(cleanupPrefs);
 
   // Should return the system downloads directory.
   Services.prefs.setIntPref(folderListPrefName, 1);
   let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
   let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
   do_check_neq(downloadDir, "");
   do_check_eq(downloadDir, systemDir);
 
@@ -169,48 +175,65 @@ add_task(function* test_getPreferredDown
   do_check_eq(downloadDir, systemDir);
 
   // Should return the system downloads directory because the folderList
   // preference is invalid
   Services.prefs.setIntPref(folderListPrefName, 999);
   downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
   do_check_eq(downloadDir, systemDir);
 
-  cleanup();
+  cleanupPrefs();
+  cleanupDirectories();
 });
 
 /**
  * Tests that the getTemporaryDownloadsDirectory returns a valid download
  * directory string path.
  */
 add_task(function* test_getTemporaryDownloadsDirectory()
 {
+  let cleanup = allowDirectoriesInTest();
+
   let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
   do_check_neq(downloadDir, "");
 
   if ("nsILocalFileMac" in Ci) {
     let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
     do_check_eq(downloadDir, preferredDownloadDir);
   } else {
     let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
     do_check_eq(downloadDir, tempDir.path);
   }
+
+  cleanup();
 });
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests DownloadObserver
 
 /**
+ * Re-enables the default observers for the following tests.
+ *
+ * This takes effect the first time a DownloadList object is created, and lasts
+ * until this test file has completed.
+ */
+add_task(function* test_observers_setup()
+{
+  DownloadIntegration.allowObservers = true;
+  do_register_cleanup(function () {
+    DownloadIntegration.allowObservers = false;
+  });
+});
+
+/**
  * Tests notifications prompts when observers are notified if there are public
  * and private active downloads.
  */
 add_task(function* test_notifications()
 {
-  enableObserversTestMode();
-
   for (let isPrivate of [false, true]) {
     mustInterruptResponses();
 
     let list = yield promiseNewList(isPrivate);
     let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let promiseAttempt1 = download1.start();
@@ -239,18 +262,16 @@ add_task(function* test_notifications()
 });
 
 /**
  * Tests that notifications prompts observers are not notified if there are no
  * public or private active downloads.
  */
 add_task(function* test_no_notifications()
 {
-  enableObserversTestMode();
-
   for (let isPrivate of [false, true]) {
     let list = yield promiseNewList(isPrivate);
     let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
     download1.start().catch(() => {});
     download2.start().catch(() => {});
 
     // Add downloads to list.
@@ -269,17 +290,16 @@ add_task(function* test_no_notifications
 });
 
 /**
  * Tests notifications prompts when observers are notified if there are public
  * and private active downloads at the same time.
  */
 add_task(function* test_mix_notifications()
 {
-  enableObserversTestMode();
   mustInterruptResponses();
 
   let publicList = yield promiseNewList();
   let privateList = yield Downloads.getList(Downloads.PRIVATE);
   let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let promiseAttempt1 = download1.start();
   let promiseAttempt2 = download2.start();
@@ -301,18 +321,16 @@ add_task(function* test_mix_notification
 });
 
 /**
  * Tests suspending and resuming as well as going offline and then online again.
  * The downloads should stop when suspending and start again when resuming.
  */
 add_task(function* test_suspend_resume()
 {
-  enableObserversTestMode();
-
   // The default wake delay is 10 seconds, so set the wake delay to be much
   // faster for these tests.
   Services.prefs.setIntPref("browser.download.manager.resumeOnWakeDelay", 5);
 
   let addDownload = function(list)
   {
     return Task.spawn(function* () {
       let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
@@ -382,17 +400,16 @@ add_task(function* test_suspend_resume()
 });
 
 /**
  * Tests both the downloads list and the in-progress downloads are clear when
  * private browsing observer is notified.
  */
 add_task(function* test_exit_private_browsing()
 {
-  enableObserversTestMode();
   mustInterruptResponses();
 
   let privateList = yield promiseNewList(true);
   let download1 = yield promiseNewDownload(httpUrl("source.txt"));
   let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
   let promiseAttempt1 = download1.start();
   let promiseAttempt2 = download2.start();
 
@@ -401,17 +418,18 @@ add_task(function* test_exit_private_bro
   yield privateList.add(download2);
 
   // Complete the download.
   yield promiseAttempt1;
 
   do_check_eq((yield privateList.getAll()).length, 2);
 
   // Simulate exiting the private browsing.
-  DownloadIntegration._deferTestClearPrivateList = Promise.defer();
-  Services.obs.notifyObservers(null, "last-pb-context-exited", null);
-  let result = yield DownloadIntegration._deferTestClearPrivateList.promise;
+  yield new Promise(resolve => {
+    DownloadIntegration._testResolveClearPrivateList = resolve;
+    Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+  });
+  delete DownloadIntegration._testResolveClearPrivateList;
 
-  do_check_eq(result, "success");
   do_check_eq((yield privateList.getAll()).length, 0);
 
   continueResponses();
 });