chromium/chrome/browser/resources/chromeos/accessibility/accessibility_common/accessibility_common_loader.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '/common/testing/test_import_manager.js';

import {Flags} from '/common/flags.js';
import {InstanceChecker} from '/common/instance_checker.js';
import {TestImportManager} from '/common/testing/test_import_manager.js';

import {Autoclick} from './autoclick/autoclick.js';
import {Dictation} from './dictation/dictation.js';
import {FaceGaze} from './facegaze/facegaze.js';
import {Magnifier} from './magnifier/magnifier.js';

declare global {
  var accessibilityCommon: AccessibilityCommon;
}

/**
 * Class to manage loading resources depending on which Accessibility features
 * are enabled.
 */
export class AccessibilityCommon {
  private autoclick_: Autoclick|null = null;
  private magnifier_: Magnifier|null = null;
  private dictation_: Dictation|null = null;
  private faceGaze_: FaceGaze|null = null;

  // For tests.
  private autoclickLoadCallbackForTest_: Function|null = null;
  // TODO(b:315990318): Migrate these callbacks to Function after
  // setOnLoadDesktopCallbackForTest() is migrated to typescript.
  private magnifierLoadCallbackForTest_: (() => void)|null = null;
  private dictationLoadCallbackForTest_: Function|null = null;
  private facegazeLoadCallbackForTest_: Function|null = null;

  static readonly FACEGAZE_PREF_NAME = 'settings.a11y.face_gaze.enabled';


  constructor() {
    this.init_();
  }

  static async init(): Promise<void> {
    await Flags.init();
    globalThis.accessibilityCommon = new AccessibilityCommon();
  }

  getAutoclickForTest(): Autoclick|null {
    return this.autoclick_;
  }

  getFaceGazeForTest(): FaceGaze|null {
    return this.faceGaze_;
  }

  getMagnifierForTest(): Magnifier|null {
    return this.magnifier_;
  }

  /**
   * Initializes the AccessibilityCommon extension.
   */
  private init_(): void {
    chrome.accessibilityFeatures.autoclick.get(
        {}, details => this.onAutoclickUpdated_(details));
    chrome.accessibilityFeatures.autoclick.onChange.addListener(
        details => this.onAutoclickUpdated_(details));

    chrome.accessibilityFeatures.screenMagnifier.get(
        {},
        details =>
            this.onMagnifierUpdated_(Magnifier.Type.FULL_SCREEN, details));
    chrome.accessibilityFeatures.screenMagnifier.onChange.addListener(
        details =>
            this.onMagnifierUpdated_(Magnifier.Type.FULL_SCREEN, details));

    chrome.accessibilityFeatures.dockedMagnifier.get(
        {},
        details => this.onMagnifierUpdated_(Magnifier.Type.DOCKED, details));
    chrome.accessibilityFeatures.dockedMagnifier.onChange.addListener(
        details => this.onMagnifierUpdated_(Magnifier.Type.DOCKED, details));

    chrome.accessibilityFeatures.dictation.get(
        {}, details => this.onDictationUpdated_(details));
    chrome.accessibilityFeatures.dictation.onChange.addListener(
        details => this.onDictationUpdated_(details));

    const faceGazeFeature =
        chrome.accessibilityPrivate.AccessibilityFeature.FACE_GAZE;
    chrome.accessibilityPrivate.isFeatureEnabled(faceGazeFeature, enabled => {
      if (!enabled) {
        return;
      }
      // TODO(b/309121742): Add FaceGaze pref to the accessibilityFeatures
      // extension API.
      chrome.settingsPrivate.getPref(
          AccessibilityCommon.FACEGAZE_PREF_NAME,
          pref => this.onFaceGazeUpdated_(pref));
      chrome.settingsPrivate.onPrefsChanged.addListener(prefs => {
        for (const pref of prefs) {
          if (pref.key === AccessibilityCommon.FACEGAZE_PREF_NAME) {
            this.onFaceGazeUpdated_(pref);
            break;
          }
        }
      });
    });

    // AccessibilityCommon is an IME so it shows in the input methods list
    // when it starts up. Remove from this list, Dictation will add it back
    // whenever needed.
    Dictation.removeAsInputMethod();
  }

  /**
   * Called when the autoclick feature is enabled or disabled.
   */
  private onAutoclickUpdated_(
      details: chrome.accessibilityFeatures.ChromeSettingsResponse): void {
    if (details.value && !this.autoclick_) {
      // Initialize the Autoclick extension.
      this.autoclick_ = new Autoclick();
      if (this.autoclickLoadCallbackForTest_) {
        this.autoclick_.setOnLoadDesktopCallbackForTest(
            this.autoclickLoadCallbackForTest_);
        this.autoclickLoadCallbackForTest_ = null;
      }
    } else if (!details.value && this.autoclick_) {
      // TODO(crbug.com/1096759): Consider using XHR to load/unload autoclick
      // rather than relying on a destructor to clean up state.
      this.autoclick_.onAutoclickDisabled();
      this.autoclick_ = null;
    }
  }

  /**
   * Called when the FaceGaze feature is fetched enabled or disabled.
   */
  private onFaceGazeUpdated_(details: chrome.settingsPrivate.PrefObject): void {
    if (details.value && !this.faceGaze_) {
      // Initialize the FaceGaze extension.
      this.faceGaze_ = new FaceGaze();
      if (this.facegazeLoadCallbackForTest_) {
        this.facegazeLoadCallbackForTest_();
        this.facegazeLoadCallbackForTest_ = null;
      }
    } else if (!details.value && this.faceGaze_) {
      this.faceGaze_.onFaceGazeDisabled();
      this.faceGaze_ = null;
    }
  }

  /**
   * Called when the magnifier feature is fetched enabled or disabled.
   */
  private onMagnifierUpdated_(
      type: Magnifier.Type,
      details: chrome.accessibilityFeatures.ChromeSettingsResponse): void {
    if (details.value && !this.magnifier_) {
      this.magnifier_ = new Magnifier(type);
      if (this.magnifierLoadCallbackForTest_) {
        this.magnifier_.setOnLoadDesktopCallbackForTest(
            this.magnifierLoadCallbackForTest_);
        this.magnifierLoadCallbackForTest_ = null;
      }
    } else if (
        !details.value && this.magnifier_ && this.magnifier_.type === type) {
      this.magnifier_.onMagnifierDisabled();
      this.magnifier_ = null;
    }
  }

  /**
   * Called when the dictation feature is enabled or disabled.
   */
  private onDictationUpdated_(
      details: chrome.accessibilityFeatures.ChromeSettingsResponse): void {
    if (details.value && !this.dictation_) {
      this.dictation_ = new Dictation();
      if (this.dictationLoadCallbackForTest_) {
        this.dictationLoadCallbackForTest_();
        this.dictationLoadCallbackForTest_ = null;
      }
    } else if (!details.value && this.dictation_) {
      this.dictation_.onDictationDisabled();
      this.dictation_ = null;
    }
  }

  /**
   * Used by C++ tests to ensure a feature load is completed.
   * Set on AccessibilityCommon in case the feature has not started up yet.
   */
  setFeatureLoadCallbackForTest(feature: string, callback: () => void): void {
    if (feature === 'autoclick') {
      if (!this.autoclick_) {
        this.autoclickLoadCallbackForTest_ = callback;
        return;
      }
      // Autoclick already loaded.
      this.autoclick_.setOnLoadDesktopCallbackForTest(callback);
    } else if (feature === 'dictation') {
      if (!this.dictation_) {
        this.dictationLoadCallbackForTest_ = callback;
        return;
      }
      // Dictation already loaded.
      callback();
    } else if (feature === 'magnifier') {
      if (!this.magnifier_) {
        this.magnifierLoadCallbackForTest_ = callback;
        return;
      }
      // Magnifier already loaded.
      this.magnifier_.setOnLoadDesktopCallbackForTest(callback);
    } else if (feature === 'facegaze') {
      if (!this.faceGaze_) {
        this.facegazeLoadCallbackForTest_ = callback;
        return;
      }
      // Facegaze already loaded.
      callback();
    }
  }
}


InstanceChecker.closeExtraInstances();
// Initialize the AccessibilityCommon extension.
AccessibilityCommon.init();

TestImportManager.exportForTesting(
    ['AccessibilityCommon', AccessibilityCommon]);