/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
  GMPInstallManager: "resource://gre/modules/GMPInstallManager.sys.mjs",
  Log: "resource://gre/modules/Log.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

import {
  GMPPrefs,
  GMPUtils,
  OPEN_H264_ID,
  WIDEVINE_L1_ID,
  WIDEVINE_L3_ID,
} from "resource://gre/modules/GMPUtils.sys.mjs";

const SEC_IN_A_DAY = 24 * 60 * 60;
// How long to wait after a user enabled EME before attempting to download CDMs.
const GMP_CHECK_DELAY = 10 * 1000; // milliseconds

const XHTML = "http://www.w3.org/1999/xhtml";

const NS_GRE_DIR = "GreD";
const CLEARKEY_PLUGIN_ID = "gmp-clearkey";
const CLEARKEY_VERSION = "0.1";

const FIRST_CONTENT_PROCESS_TOPIC = "ipc:first-content-process-created";

const GMP_LICENSE_INFO = "plugins-gmp-license-info";
const GMP_PRIVACY_INFO = "plugins-gmp-privacy-info";
const GMP_LEARN_MORE = "learn_more_label";

const GMP_PLUGINS = [
  {
    id: OPEN_H264_ID,
    name: "plugins-openh264-name",
    description: "plugins-openh264-description",
    level: "",
    libName: "gmpopenh264",
    // The following licenseURL is part of an awful hack to include the OpenH264
    // license without having bug 624602 fixed yet, and intentionally ignores
    // localisation.
    licenseURL: "chrome://mozapps/content/extensions/OpenH264-license.txt",
    homepageURL: "https://www.openh264.org/",
  },
  {
    id: WIDEVINE_L1_ID,
    name: "plugins-widevine-name",
    description: "plugins-widevine-description",
    level: "L1",
    libName: "Google.Widevine.CDM",
    licenseURL: "https://www.google.com/policies/privacy/",
    homepageURL: "https://www.widevine.com/",
    isEME: true,
  },
  {
    id: WIDEVINE_L3_ID,
    name: "plugins-widevine-name",
    description: "plugins-widevine-description",
    level: "L3",
    libName: "widevinecdm",
    licenseURL: "https://www.google.com/policies/privacy/",
    homepageURL: "https://www.widevine.com/",
    isEME: true,
  },
];

ChromeUtils.defineLazyGetter(
  lazy,
  "addonsBundle",
  () => new Localization(["toolkit/about/aboutAddons.ftl"], true)
);
ChromeUtils.defineLazyGetter(lazy, "gmpService", () =>
  Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(
    Ci.mozIGeckoMediaPluginChromeService
  )
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "gmpProviderEnabled",
  GMPPrefs.KEY_PROVIDER_ENABLED
);

var gLogger;
var gLogAppenderDump = null;

function configureLogging() {
  if (!gLogger) {
    gLogger = lazy.Log.repository.getLogger("Toolkit.GMP");
    gLogger.addAppender(
      new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
    );
  }
  gLogger.level = GMPPrefs.getInt(
    GMPPrefs.KEY_LOGGING_LEVEL,
    lazy.Log.Level.Warn
  );

  let logDumping = GMPPrefs.getBool(GMPPrefs.KEY_LOGGING_DUMP, false);
  if (logDumping != !!gLogAppenderDump) {
    if (logDumping) {
      gLogAppenderDump = new lazy.Log.DumpAppender(
        new lazy.Log.BasicFormatter()
      );
      gLogger.addAppender(gLogAppenderDump);
    } else {
      gLogger.removeAppender(gLogAppenderDump);
      gLogAppenderDump = null;
    }
  }
}

/**
 * The GMPWrapper provides the info for the various GMP plugins to public
 * callers through the API.
 */
function GMPWrapper(aPluginInfo, aRawPluginInfo) {
  this._plugin = aPluginInfo;
  this._rawPlugin = aRawPluginInfo;
  this._log = lazy.Log.repository.getLoggerWithMessagePrefix(
    "Toolkit.GMP",
    "GMPWrapper(" + this._plugin.id + ") "
  );
  Services.prefs.addObserver(
    GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, this._plugin.id),
    this,
    true
  );
  Services.prefs.addObserver(
    GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, this._plugin.id),
    this,
    true
  );
  if (this._plugin.isEME) {
    Services.prefs.addObserver(GMPPrefs.KEY_EME_ENABLED, this, true);
    Services.obs.addObserver(this, "EMEVideo:CDMMissing");
  }
}

GMPWrapper.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIObserver",
    "nsISupportsWeakReference",
  ]),

  // An active task that checks for plugin updates and installs them.
  _updateTask: null,
  _gmpPath: null,
  _isUpdateCheckPending: false,

  set gmpPath(aPath) {
    this._gmpPath = aPath;
  },
  get gmpPath() {
    if (!this._gmpPath && this.isInstalled) {
      this._gmpPath = PathUtils.join(
        Services.dirsvc.get("ProfD", Ci.nsIFile).path,
        this._plugin.id,
        GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, null, this._plugin.id)
      );
    }
    return this._gmpPath;
  },

  get id() {
    return this._plugin.id;
  },
  get libName() {
    return this._plugin.libName;
  },
  get type() {
    return "plugin";
  },
  get isGMPlugin() {
    return true;
  },
  get name() {
    return this._plugin.name;
  },
  get creator() {
    return null;
  },
  get homepageURL() {
    return this._plugin.homepageURL;
  },

  get description() {
    return this._plugin.description;
  },
  get fullDescription() {
    return null;
  },

  getFullDescription(doc) {
    let plugin = this._rawPlugin;

    let frag = doc.createDocumentFragment();
    for (let [urlProp, labelId] of [
      ["learnMoreURL", GMP_LEARN_MORE],
      [
        "licenseURL",
        this.id == WIDEVINE_L1_ID || this.id == WIDEVINE_L3_ID
          ? GMP_PRIVACY_INFO
          : GMP_LICENSE_INFO,
      ],
    ]) {
      if (plugin[urlProp]) {
        let a = doc.createElementNS(XHTML, "a");
        a.href = plugin[urlProp];
        a.target = "_blank";
        a.textContent = lazy.addonsBundle.formatValueSync(labelId);

        if (frag.childElementCount) {
          frag.append(
            doc.createElementNS(XHTML, "br"),
            doc.createElementNS(XHTML, "br")
          );
        }
        frag.append(a);
      }
    }

    return frag;
  },

  get version() {
    return GMPPrefs.getString(
      GMPPrefs.KEY_PLUGIN_VERSION,
      null,
      this._plugin.id
    );
  },

  get isActive() {
    return (
      !this.appDisabled &&
      !this.userDisabled &&
      !GMPUtils.isPluginHidden(this._plugin)
    );
  },
  get appDisabled() {
    if (
      this._plugin.isEME &&
      !GMPPrefs.getBool(GMPPrefs.KEY_EME_ENABLED, true)
    ) {
      // If "media.eme.enabled" is false, all EME plugins are disabled.
      return true;
    }
    return false;
  },

  get userDisabled() {
    return !GMPPrefs.getBool(
      GMPPrefs.KEY_PLUGIN_ENABLED,
      true,
      this._plugin.id
    );
  },
  set userDisabled(aVal) {
    GMPPrefs.setBool(
      GMPPrefs.KEY_PLUGIN_ENABLED,
      aVal === false,
      this._plugin.id
    );
  },

  async enable() {
    this.userDisabled = false;
  },
  async disable() {
    this.userDisabled = true;
  },

  get blocklistState() {
    return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
  },
  get size() {
    return 0;
  },
  get scope() {
    return lazy.AddonManager.SCOPE_APPLICATION;
  },
  get pendingOperations() {
    return lazy.AddonManager.PENDING_NONE;
  },

  get operationsRequiringRestart() {
    return lazy.AddonManager.OP_NEEDS_RESTART_NONE;
  },

  get permissions() {
    let permissions = 0;
    if (!this.appDisabled) {
      permissions |= lazy.AddonManager.PERM_CAN_UPGRADE;
      permissions |= this.userDisabled
        ? lazy.AddonManager.PERM_CAN_ENABLE
        : lazy.AddonManager.PERM_CAN_DISABLE;
    }
    return permissions;
  },

  get updateDate() {
    let time = Number(
      GMPPrefs.getInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, 0, this._plugin.id)
    );
    if (this.isInstalled) {
      return new Date(time * 1000);
    }
    return null;
  },

  get isCompatible() {
    return true;
  },

  get isPlatformCompatible() {
    return true;
  },

  get providesUpdatesSecurely() {
    return true;
  },

  get foreignInstall() {
    return false;
  },

  get installTelemetryInfo() {
    return { source: "gmp-plugin" };
  },

  isCompatibleWith() {
    return true;
  },

  get applyBackgroundUpdates() {
    if (!GMPPrefs.isSet(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id)) {
      return lazy.AddonManager.AUTOUPDATE_DEFAULT;
    }

    return GMPPrefs.getBool(
      GMPPrefs.KEY_PLUGIN_AUTOUPDATE,
      true,
      this._plugin.id
    )
      ? lazy.AddonManager.AUTOUPDATE_ENABLE
      : lazy.AddonManager.AUTOUPDATE_DISABLE;
  },

  set applyBackgroundUpdates(aVal) {
    if (aVal == lazy.AddonManager.AUTOUPDATE_DEFAULT) {
      GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, this._plugin.id);
    } else if (aVal == lazy.AddonManager.AUTOUPDATE_ENABLE) {
      GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, this._plugin.id);
    } else if (aVal == lazy.AddonManager.AUTOUPDATE_DISABLE) {
      GMPPrefs.setBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, this._plugin.id);
    }
  },

  /**
   * Called by the addon manager to update GMP addons. For example this will be
   * used if a user manually checks for GMP plugin updates by using the
   * menu in about:addons.
   *
   * This function is not used if MediaKeySystemAccess is requested and
   * Widevine is not yet installed, or if the user toggles prefs to enable EME.
   * For the function used in those cases see `checkForUpdates`.
   */
  findUpdates(aListener, aReason) {
    this._log.trace(
      "findUpdates() - " + this._plugin.id + " - reason=" + aReason
    );

    // In the case of GMP addons we do not wish to implement AddonInstall, as
    // we don't want to display information as in a normal addon install such
    // as a download progress bar. As such, we short circuit our
    // listeners by indicating that no updates exist (though some may).
    lazy.AddonManagerPrivate.callNoUpdateListeners(this, aListener);

    if (aReason === lazy.AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
      if (!lazy.AddonManager.shouldAutoUpdate(this)) {
        this._log.trace(
          "findUpdates() - " + this._plugin.id + " - no autoupdate"
        );
        return Promise.resolve(false);
      }

      let secSinceLastCheck =
        Date.now() / 1000 -
        Services.prefs.getIntPref(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
      if (secSinceLastCheck <= SEC_IN_A_DAY) {
        this._log.trace(
          "findUpdates() - " +
            this._plugin.id +
            " - last check was less then a day ago"
        );
        return Promise.resolve(false);
      }
    } else if (aReason !== lazy.AddonManager.UPDATE_WHEN_USER_REQUESTED) {
      this._log.trace(
        "findUpdates() - " +
          this._plugin.id +
          " - the given reason to update is not supported"
      );
      return Promise.resolve(false);
    }

    if (this._updateTask !== null) {
      this._log.trace(
        "findUpdates() - " + this._plugin.id + " - update task already running"
      );
      return this._updateTask;
    }

    this._updateTask = (async () => {
      this._log.trace("findUpdates() - updateTask");
      try {
        let installManager = new lazy.GMPInstallManager();
        let res = await installManager.checkForAddons();
        let update = res.addons.find(addon => addon.id === this._plugin.id);
        if (update && update.isValid && !update.isInstalled) {
          this._log.trace(
            "findUpdates() - found update for " +
              this._plugin.id +
              ", installing"
          );
          await installManager.installAddon(update);
        } else {
          this._log.trace("findUpdates() - no updates for " + this._plugin.id);
        }
        this._log.info(
          "findUpdates() - updateTask succeeded for " + this._plugin.id
        );
      } catch (e) {
        this._log.error(
          "findUpdates() - updateTask for " + this._plugin.id + " threw",
          e
        );
        throw e;
      } finally {
        this._updateTask = null;
      }
      return true;
    })();

    return this._updateTask;
  },

  get pluginLibraries() {
    if (this.isInstalled) {
      let path = this.version;
      return [path];
    }
    return [];
  },
  get pluginFullpath() {
    if (this.isInstalled) {
      let path = PathUtils.join(
        Services.dirsvc.get("ProfD", Ci.nsIFile).path,
        this._plugin.id,
        this.version
      );
      return [path];
    }
    return [];
  },

  get isInstalled() {
    return this.version && !!this.version.length;
  },

  _handleEnabledChanged() {
    this._log.info(
      "_handleEnabledChanged() id=" +
        this._plugin.id +
        " isActive=" +
        this.isActive
    );

    lazy.AddonManagerPrivate.callAddonListeners(
      this.isActive ? "onEnabling" : "onDisabling",
      this,
      false
    );
    if (this._gmpPath) {
      if (this.isActive) {
        this._log.info(
          "onPrefEnabledChanged() - adding gmp directory " + this._gmpPath
        );
        lazy.gmpService.addPluginDirectory(this._gmpPath);
      } else {
        this._log.info(
          "onPrefEnabledChanged() - removing gmp directory " + this._gmpPath
        );
        lazy.gmpService.removePluginDirectory(this._gmpPath);
      }
    }
    lazy.AddonManagerPrivate.callAddonListeners(
      this.isActive ? "onEnabled" : "onDisabled",
      this
    );
  },

  onPrefEMEGlobalEnabledChanged() {
    this._log.info(
      "onPrefEMEGlobalEnabledChanged() id=" +
        this._plugin.id +
        " appDisabled=" +
        this.appDisabled +
        " isActive=" +
        this.isActive +
        " hidden=" +
        GMPUtils.isPluginHidden(this._plugin)
    );

    lazy.AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
      "appDisabled",
    ]);
    // If EME or the GMP itself are disabled, uninstall the GMP.
    // Otherwise, check for updates, so we download and install the GMP.
    if (this.appDisabled) {
      this.uninstallPlugin();
    } else if (!GMPUtils.isPluginHidden(this._plugin)) {
      lazy.AddonManagerPrivate.callInstallListeners(
        "onExternalInstall",
        null,
        this,
        null,
        false
      );
      lazy.AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
      lazy.AddonManagerPrivate.callAddonListeners("onInstalled", this);
      this.checkForUpdates(GMP_CHECK_DELAY);
    }
    if (!this.userDisabled) {
      this._handleEnabledChanged();
    }
  },

  /**
   * This is called if prefs are changed to enable EME, or if Widevine
   * MediaKeySystemAccess is requested but the Widevine CDM is not installed.
   *
   * For the function used by the addon manager see `findUpdates`.
   */
  checkForUpdates(delay) {
    if (this._isUpdateCheckPending) {
      return;
    }
    this._isUpdateCheckPending = true;
    GMPPrefs.reset(GMPPrefs.KEY_UPDATE_LAST_CHECK, null);
    // Delay this in case the user changes his mind and doesn't want to
    // enable EME after all.
    lazy.setTimeout(() => {
      if (!this.appDisabled && !Services.startup.shuttingDown) {
        let gmpInstallManager = new lazy.GMPInstallManager();
        // We don't really care about the results, if someone is interested
        // they can check the log.
        gmpInstallManager.simpleCheckAndInstall().catch(() => {});
      }
      this._isUpdateCheckPending = false;
    }, delay);
  },

  onPrefEnabledChanged() {
    if (!this._plugin.isEME || !this.appDisabled) {
      this._handleEnabledChanged();
    }
  },

  onPrefVersionChanged() {
    lazy.AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
    if (this._gmpPath) {
      this._log.info(
        "onPrefVersionChanged() - unregistering gmp directory " + this._gmpPath
      );
      lazy.gmpService.removeAndDeletePluginDirectory(
        this._gmpPath,
        true /* can defer */
      );
    }
    lazy.AddonManagerPrivate.callAddonListeners("onUninstalled", this);

    lazy.AddonManagerPrivate.callInstallListeners(
      "onExternalInstall",
      null,
      this,
      null,
      false
    );
    lazy.AddonManagerPrivate.callAddonListeners("onInstalling", this, false);
    this._gmpPath = null;
    if (this.isInstalled) {
      this._gmpPath = PathUtils.join(
        Services.dirsvc.get("ProfD", Ci.nsIFile).path,
        this._plugin.id,
        GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, null, this._plugin.id)
      );
    }
    if (this._gmpPath && this.isActive) {
      this._log.info(
        "onPrefVersionChanged() - registering gmp directory " + this._gmpPath
      );
      lazy.gmpService.addPluginDirectory(this._gmpPath);
    }
    lazy.AddonManagerPrivate.callAddonListeners("onInstalled", this);
  },

  observe(subject, topic, data) {
    if (topic == "nsPref:changed") {
      let pref = data;
      if (
        pref ==
        GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, this._plugin.id)
      ) {
        this.onPrefEnabledChanged();
      } else if (
        pref ==
        GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, this._plugin.id)
      ) {
        this.onPrefVersionChanged();
      } else if (pref == GMPPrefs.KEY_EME_ENABLED) {
        this.onPrefEMEGlobalEnabledChanged();
      }
    } else if (topic == "EMEVideo:CDMMissing") {
      this.checkForUpdates(0);
    }
  },

  uninstallPlugin() {
    lazy.AddonManagerPrivate.callAddonListeners("onUninstalling", this, false);
    if (this.gmpPath) {
      this._log.info(
        "uninstallPlugin() - unregistering gmp directory " + this.gmpPath
      );
      lazy.gmpService.removeAndDeletePluginDirectory(this.gmpPath);
    }
    GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_VERSION, this.id);
    GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_HASHVALUE, this.id);
    GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_ABI, this.id);
    GMPPrefs.reset(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, this.id);
    lazy.AddonManagerPrivate.callAddonListeners("onUninstalled", this);
  },

  shutdown() {
    Services.prefs.removeObserver(
      GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_ENABLED, this._plugin.id),
      this
    );
    Services.prefs.removeObserver(
      GMPPrefs.getPrefKey(GMPPrefs.KEY_PLUGIN_VERSION, this._plugin.id),
      this
    );
    if (this._plugin.isEME) {
      Services.prefs.removeObserver(GMPPrefs.KEY_EME_ENABLED, this);
      Services.obs.removeObserver(this, "EMEVideo:CDMMissing");
    }
    return this._updateTask;
  },

  _arePluginFilesOnDisk() {
    let fileExists = function (aGmpPath, aFileName) {
      let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
      let path = PathUtils.join(aGmpPath, aFileName);
      f.initWithPath(path);
      return f.exists();
    };

    let libName =
      AppConstants.DLL_PREFIX + this._plugin.libName + AppConstants.DLL_SUFFIX;
    let infoName;
    if (
      this._plugin.id == WIDEVINE_L1_ID ||
      this._plugin.id == WIDEVINE_L3_ID
    ) {
      infoName = "manifest.json";
    } else {
      infoName = this._plugin.id.substring(4) + ".info";
    }

    return (
      fileExists(this.gmpPath, libName) && fileExists(this.gmpPath, infoName)
    );
  },

  validate() {
    if (!this.isInstalled) {
      // Not installed -> Valid.
      return {
        installed: false,
        valid: true,
      };
    }

    let expectedABI = GMPUtils._expectedABI(this._plugin);
    let abi = GMPPrefs.getString(
      GMPPrefs.KEY_PLUGIN_ABI,
      expectedABI,
      this._plugin.id
    );
    if (abi != expectedABI) {
      // ABI doesn't match. Possibly this is a profile migrated across platforms
      // or from 32 -> 64 bit.
      return {
        installed: true,
        mismatchedABI: true,
        valid: false,
      };
    }

    // Installed -> Check if files are missing.
    let filesOnDisk = this._arePluginFilesOnDisk();
    return {
      installed: true,
      valid: filesOnDisk,
    };
  },
};

var GMPProvider = {
  get name() {
    return "GMPProvider";
  },

  _plugins: null,

  startup() {
    configureLogging();
    this._log = lazy.Log.repository.getLoggerWithMessagePrefix(
      "Toolkit.GMP",
      "GMPProvider."
    );
    this.buildPluginList();
    this.ensureProperCDMInstallState();

    Services.prefs.addObserver(GMPPrefs.KEY_LOG_BASE, configureLogging);

    for (let plugin of this._plugins.values()) {
      let wrapper = plugin.wrapper;
      let gmpPath = wrapper.gmpPath;
      let isEnabled = wrapper.isActive;
      this._log.trace(
        "startup - enabled=" + isEnabled + ", gmpPath=" + gmpPath
      );

      if (gmpPath && isEnabled) {
        let validation = wrapper.validate();
        if (validation.mismatchedABI) {
          this._log.info(
            "startup - gmp " + plugin.id + " mismatched ABI, uninstalling"
          );
          wrapper.uninstallPlugin();
          continue;
        }
        if (!validation.valid) {
          this._log.info(
            "startup - gmp " + plugin.id + " invalid, uninstalling"
          );
          wrapper.uninstallPlugin();
          continue;
        }
        this._log.info("startup - adding gmp directory " + gmpPath);
        try {
          lazy.gmpService.addPluginDirectory(gmpPath);
        } catch (e) {
          if (e.name != "NS_ERROR_NOT_AVAILABLE") {
            throw e;
          }
          this._log.warn(
            "startup - adding gmp directory failed with " +
              e.name +
              " - sandboxing not available?",
            e
          );
        }
      }
    }

    try {
      let greDir = Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile);
      let path = greDir.path;
      if (
        GMPUtils._isWindowsOnARM64() &&
        GMPPrefs.getBool(
          GMPPrefs.KEY_PLUGIN_ALLOW_X64_ON_ARM64,
          true,
          CLEARKEY_PLUGIN_ID
        )
      ) {
        path = PathUtils.join(path, "i686");
      }
      let clearkeyPath = PathUtils.join(
        path,
        CLEARKEY_PLUGIN_ID,
        CLEARKEY_VERSION
      );
      this._log.info("startup - adding clearkey CDM directory " + clearkeyPath);
      lazy.gmpService.addPluginDirectory(clearkeyPath);
    } catch (e) {
      this._log.warn("startup - adding clearkey CDM failed", e);
    }
  },

  shutdown() {
    this._log.trace("shutdown");
    Services.prefs.removeObserver(GMPPrefs.KEY_LOG_BASE, configureLogging);

    let shutdownTask = (async () => {
      this._log.trace("shutdown - shutdownTask");
      let shutdownSucceeded = true;

      for (let plugin of this._plugins.values()) {
        try {
          await plugin.wrapper.shutdown();
        } catch (e) {
          shutdownSucceeded = false;
        }
      }

      this._plugins = null;

      if (!shutdownSucceeded) {
        throw new Error("Shutdown failed");
      }
    })();

    return shutdownTask;
  },

  async getAddonByID(aId) {
    if (!this.isEnabled) {
      return null;
    }

    let plugin = this._plugins.get(aId);
    if (plugin && !GMPUtils.isPluginHidden(plugin)) {
      return plugin.wrapper;
    }
    return null;
  },

  async getAddonsByTypes(aTypes) {
    if (!this.isEnabled || (aTypes && !aTypes.includes("plugin"))) {
      return [];
    }

    let results = Array.from(this._plugins.values())
      .filter(p => !GMPUtils.isPluginHidden(p))
      .map(p => p.wrapper);

    return results;
  },

  get isEnabled() {
    return lazy.gmpProviderEnabled;
  },

  buildPluginList() {
    this._plugins = new Map();
    for (let aPlugin of GMP_PLUGINS) {
      let plugin = {
        id: aPlugin.id,
        name: lazy.addonsBundle.formatValueSync(aPlugin.name),
        description: lazy.addonsBundle.formatValueSync(aPlugin.description),
        libName: aPlugin.libName,
        homepageURL: aPlugin.homepageURL,
        optionsURL: aPlugin.optionsURL,
        wrapper: null,
        isEME: aPlugin.isEME,
      };
      plugin.wrapper = new GMPWrapper(plugin, aPlugin);
      this._plugins.set(plugin.id, plugin);
    }
  },

  ensureProperCDMInstallState() {
    if (!GMPPrefs.getBool(GMPPrefs.KEY_EME_ENABLED, true)) {
      for (let plugin of this._plugins.values()) {
        if (plugin.isEME && plugin.wrapper.isInstalled) {
          lazy.gmpService.addPluginDirectory(plugin.wrapper.gmpPath);
          plugin.wrapper.uninstallPlugin();
        }
      }
    }
  },

  observe(subject, topic) {
    if (topic == FIRST_CONTENT_PROCESS_TOPIC) {
      lazy.AddonManagerPrivate.registerProvider(GMPProvider, ["plugin"]);
      Services.obs.notifyObservers(null, "gmp-provider-registered");

      Services.obs.removeObserver(this, FIRST_CONTENT_PROCESS_TOPIC);
    }
  },

  addObserver() {
    Services.obs.addObserver(this, FIRST_CONTENT_PROCESS_TOPIC);
  },
};

GMPProvider.addObserver();

// For test use only.
export const GMPTestUtils = {
  /**
   * Used to override the GMP service with a mock.
   *
   * @param {object} mockService
   *        The mocked gmpService object.
   * @param {function} callback
   *        Method called with the overridden gmpService. The override
   *        is undone after the callback returns.
   */
  async overrideGmpService(mockService, callback) {
    let originalGmpService = lazy.gmpService;
    lazy.gmpService = mockService;
    try {
      return await callback();
    } finally {
      lazy.gmpService = originalGmpService;
    }
  },
};
