chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/earcons.js

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

/**
 * @fileoverview Earcons library that uses EarconEngine to play back
 * auditory cues.
 */

import {EarconId} from '../common/earcon_id.js';
import {LogType} from '../common/log_types.js';
import {Msgs} from '../common/msgs.js';
import {SettingsManager} from '../common/settings_manager.js';
import {Personality, QueueMode} from '../common/tts_types.js';

import {AbstractEarcons} from './abstract_earcons.js';
import {ChromeVox} from './chromevox.js';
import {ChromeVoxRange} from './chromevox_range.js';
import {EarconEngine} from './earcon_engine.js';
import {LogStore} from './logging/log_store.js';

const DeviceType = chrome.audio.DeviceType;

/**
 * High-level class that manages when each earcon should start (and when
 * relevant, stop) playing.
 */
export class Earcons extends AbstractEarcons {
  constructor() {
    super();

    /**
     * @type {EarconEngine}
     * @private
     */
    this.engine_ = new EarconEngine();

    /** @private {boolean} */
    this.shouldPan_ = true;

    if (chrome.audio) {
      chrome.audio.getDevices(
          {isActive: true, streamTypes: [chrome.audio.StreamType.OUTPUT]},
          devices => this.updateShouldPanForDevices_(devices));
      chrome.audio.onDeviceListChanged.addListener(
          devices => this.updateShouldPanForDevices_(devices));
    } else {
      this.shouldPan_ = false;
    }
  }

  /**
   * @return {string} The human-readable name of the earcon set.
   */
  getName() {
    return 'ChromeVox earcons';
  }

  /**
   * Plays the specified earcon sound.
   * @param {EarconId} earcon An earcon identifier.
   * @param {chrome.automation.Rect=} opt_location A location associated with
   *     the earcon such as a control's bounding rectangle.
   * @override
   */
  playEarcon(earcon, opt_location) {
    if (!this.enabled) {
      return;
    }
    if (SettingsManager.getBoolean('enableEarconLogging')) {
      LogStore.instance.writeTextLog(earcon, LogType.EARCON);
      console.log('Earcon ' + earcon);
    }
    if (ChromeVoxRange.current?.isValid()) {
      const node = ChromeVoxRange.current.start.node;
      const rect = opt_location ?? node.location;
      const container = node.root.location;
      if (this.shouldPan_) {
        this.engine_.setPositionForRect(rect, container);
      } else {
        this.engine_.resetPan();
      }
    }

    this.engine_.playEarcon(earcon);
  }

  /** @override */
  cancelEarcon(earcon) {
    switch (earcon) {
      case EarconId.PAGE_START_LOADING:
        this.engine_.cancelProgress();
        break;
    }
  }

  /** @override */
  toggle() {
    this.enabled = !this.enabled;
    const announce =
        this.enabled ? Msgs.getMsg('earcons_on') : Msgs.getMsg('earcons_off');
    ChromeVox.tts.speak(announce, QueueMode.FLUSH, Personality.ANNOTATION);
  }

  /**
   * Updates |this.shouldPan_| based on whether internal speakers are active
   * or not.
   * @param {Array<chrome.audio.AudioDeviceInfo>} devices
   * @private
   */
  updateShouldPanForDevices_(devices) {
    this.shouldPan_ = !devices.some(
        device => device.isActive &&
            device.deviceType === DeviceType.INTERNAL_SPEAKER);
  }
}