/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Actor } = require("resource://devtools/shared/protocol.js");
const {
  parentAccessibilitySpec,
} = require("resource://devtools/shared/specs/accessibility.js");

const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";

class ParentAccessibilityActor extends Actor {
  constructor(conn) {
    super(conn, parentAccessibilitySpec);

    this.userPref = Services.prefs.getIntPref(
      PREF_ACCESSIBILITY_FORCE_DISABLED
    );

    if (this.enabled && !this.accService) {
      // Set a local reference to an accessibility service if accessibility was
      // started elsewhere to ensure that parent process a11y service does not
      // get GC'ed away.
      this.accService = Cc["@mozilla.org/accessibilityService;1"].getService(
        Ci.nsIAccessibilityService
      );
    }

    Services.obs.addObserver(this, "a11y-consumers-changed");
    Services.prefs.addObserver(PREF_ACCESSIBILITY_FORCE_DISABLED, this);
  }

  bootstrap() {
    return {
      canBeDisabled: this.canBeDisabled,
      canBeEnabled: this.canBeEnabled,
    };
  }

  observe(subject, topic, data) {
    if (topic === "a11y-consumers-changed") {
      // This event is fired when accessibility service consumers change. Since
      // this observer lives in parent process there are 2 possible consumers of
      // a11y service: XPCOM and PlatformAPI (e.g. screen readers). We only care
      // about PlatformAPI consumer changes because when set, we can no longer
      // disable accessibility service.
      const { PlatformAPI } = JSON.parse(data);
      this.emit("can-be-disabled-change", !PlatformAPI);
    } else if (
      !this.disabling &&
      topic === "nsPref:changed" &&
      data === PREF_ACCESSIBILITY_FORCE_DISABLED
    ) {
      // PREF_ACCESSIBILITY_FORCE_DISABLED preference change event. When set to
      // >=1, it means that the user wants to disable accessibility service and
      // prevent it from starting in the future. Note: we also check
      // this.disabling state when handling this pref change because this is how
      // we disable the accessibility inspector itself.
      this.emit("can-be-enabled-change", this.canBeEnabled);
    }
  }

  /**
   * A getter that indicates if accessibility service is enabled.
   *
   * @return {boolean}
   *         True if accessibility service is on.
   */
  get enabled() {
    return Services.appinfo.accessibilityEnabled;
  }

  /**
   * A getter that indicates if the accessibility service can be disabled.
   *
   * @return {boolean}
   *         True if accessibility service can be disabled.
   */
  get canBeDisabled() {
    if (this.enabled) {
      const a11yService = Cc["@mozilla.org/accessibilityService;1"].getService(
        Ci.nsIAccessibilityService
      );
      const { PlatformAPI } = JSON.parse(a11yService.getConsumers());
      return !PlatformAPI;
    }

    return true;
  }

  /**
   * A getter that indicates if the accessibility service can be enabled.
   *
   * @return {boolean}
   *         True if accessibility service can be enabled.
   */
  get canBeEnabled() {
    return Services.prefs.getIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED) < 1;
  }

  /**
   * Enable accessibility service (via XPCOM service).
   */
  enable() {
    if (this.enabled || !this.canBeEnabled) {
      return;
    }

    this.accService = Cc["@mozilla.org/accessibilityService;1"].getService(
      Ci.nsIAccessibilityService
    );
  }

  /**
   * Force disable accessibility service. This method removes the reference to
   * the XPCOM a11y service object and flips the
   * PREF_ACCESSIBILITY_FORCE_DISABLED preference on and off to shutdown a11y
   * service.
   */
  disable() {
    if (!this.enabled || !this.canBeDisabled) {
      return;
    }

    this.disabling = true;
    this.accService = null;
    // Set PREF_ACCESSIBILITY_FORCE_DISABLED to 1 to force disable
    // accessibility service. This is the only way to guarantee an immediate
    // accessibility service shutdown in all processes. This also prevents
    // accessibility service from starting up in the future.
    Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
    // Set PREF_ACCESSIBILITY_FORCE_DISABLED back to previous default or user
    // set value. This will not start accessibility service until the user
    // activates it again. It simply ensures that accessibility service can
    // start again (when value is below 1).
    Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, this.userPref);
    delete this.disabling;
  }

  /**
   * Destroy the helper class, remove all listeners and if possible disable
   * accessibility service in the parent process.
   */
  destroy() {
    this.disable();
    super.destroy();
    Services.obs.removeObserver(this, "a11y-consumers-changed");
    Services.prefs.removeObserver(PREF_ACCESSIBILITY_FORCE_DISABLED, this);
  }
}

exports.ParentAccessibilityActor = ParentAccessibilityActor;
