chromium/chrome/browser/resources/ash/settings/device_page/fake_cros_audio_config.ts

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

/**
 * @fileoverview
 * Fake implementation of CrosAudioConfig for testing.
 */

import {assert} from 'chrome://resources/js/assert.js';

import {AudioDevice, AudioDeviceType, AudioEffectState, AudioSystemProperties, CrosAudioConfigInterface, MuteState} from '../mojom-webui/cros_audio_config.mojom-webui.js';

export const defaultFakeMicJack: AudioDevice = {
  id: BigInt(1),
  displayName: 'Mic Jack',
  isActive: true,
  deviceType: AudioDeviceType.kInternalMic,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeSpeakerActive: AudioDevice = {
  id: BigInt(2),
  displayName: 'Speaker',
  isActive: true,
  deviceType: AudioDeviceType.kInternalSpeaker,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotSupported,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeMicJackInactive: AudioDevice = {
  id: BigInt(3),
  displayName: 'Mic Jack',
  isActive: false,
  deviceType: AudioDeviceType.kInternalSpeaker,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotSupported,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const defaultFakeSpeaker: AudioDevice = {
  id: BigInt(4),
  displayName: 'Speaker',
  isActive: false,
  deviceType: AudioDeviceType.kInternalSpeaker,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotSupported,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeInternalFrontMic: AudioDevice = {
  id: BigInt(5),
  displayName: 'FrontMic',
  isActive: true,
  deviceType: AudioDeviceType.kFrontMic,
  noiseCancellationState: AudioEffectState.kNotEnabled,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeBluetoothMic: AudioDevice = {
  id: BigInt(6),
  displayName: 'Bluetooth Mic',
  isActive: false,
  deviceType: AudioDeviceType.kBluetoothNbMic,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeInternalMicActive: AudioDevice = {
  id: BigInt(7),
  displayName: 'Internal Mic',
  isActive: true,
  deviceType: AudioDeviceType.kInternalMic,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export const fakeBluetoothNbMicActiveHfpMicSrNotEnabled: AudioDevice = {
  id: BigInt(8),
  displayName: 'Bluetooth Nb Mic',
  isActive: true,
  deviceType: AudioDeviceType.kBluetoothNbMic,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotSupported,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotEnabled,
};

export const fakeInternalMicActiveWithStyleTransfer: AudioDevice = {
  id: BigInt(9),
  displayName: 'Internal Mic',
  isActive: true,
  deviceType: AudioDeviceType.kInternalMic,
  noiseCancellationState: AudioEffectState.kNotSupported,
  styleTransferState: AudioEffectState.kNotEnabled,
  forceRespectUiGainsState: AudioEffectState.kNotEnabled,
  hfpMicSrState: AudioEffectState.kNotSupported,
};

export interface FakePropertiesObserverInterface {
  onPropertiesUpdated(properties: AudioSystemProperties): void;
}

export const defaultFakeAudioSystemProperties: AudioSystemProperties = {
  outputDevices: [defaultFakeSpeaker, defaultFakeMicJack],
  outputVolumePercent: 75,
  inputGainPercent: 87,
  outputMuteState: MuteState.kNotMuted,
  inputDevices: [fakeInternalFrontMic, fakeBluetoothMic],
  inputMuteState: MuteState.kNotMuted,
};

/** Creates an audio device based on provided device and isActive override. */
export function createAudioDevice(
    baseDevice: AudioDevice, isActive: boolean): AudioDevice {
  assert(!!baseDevice);
  return ({...baseDevice, isActive});
}

export interface FakeCrosAudioConfigInterface extends CrosAudioConfigInterface {
  setInputGainPercent(percent: number): void;
}

export class FakeCrosAudioConfig implements FakeCrosAudioConfigInterface {
  private audioSystemProperties: AudioSystemProperties =
      defaultFakeAudioSystemProperties;
  private observers: FakePropertiesObserverInterface[] = [];

  observeAudioSystemProperties(observer: FakePropertiesObserverInterface):
      void {
    this.observers.push(observer);
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Sets the active output or input device and notifies observers.
   */
  setActiveDevice(deviceId: bigint): void {
    const isOutputDevice = !!(this.audioSystemProperties.outputDevices.find(
        (device: AudioDevice) => device.id === deviceId));
    if (isOutputDevice) {
      const devices = this.audioSystemProperties.outputDevices.map(
          (device: AudioDevice): AudioDevice =>
              createAudioDevice(device, device.id === deviceId));
      this.audioSystemProperties.outputDevices = devices;
    } else {
      // Device must be an input device otherwise an invalid device was
      // provided.
      assert(this.audioSystemProperties.inputDevices.find(
          (device: AudioDevice) => device.id === deviceId));
      const devices = this.audioSystemProperties.inputDevices.map(
          (device: AudioDevice): AudioDevice =>
              createAudioDevice(device, device.id === deviceId));
      this.audioSystemProperties.inputDevices = devices;
    }
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Sets `audioSystemProperties` to the desired properties and notifies
   * observers.
   */
  setAudioSystemProperties(properties: AudioSystemProperties): void {
    this.audioSystemProperties = properties;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /** Handle updating active input device noise cancellation state. */
  setNoiseCancellationEnabled(enabled: boolean): void {
    if (!this.audioSystemProperties.inputDevices) {
      return;
    }

    const activeIndex = this.audioSystemProperties.inputDevices.findIndex(
        (device: AudioDevice) => device.isActive &&
            device.noiseCancellationState !== AudioEffectState.kNotSupported);
    if (activeIndex === -1) {
      return;
    }
    const nextState: AudioEffectState =
        enabled ? AudioEffectState.kEnabled : AudioEffectState.kNotEnabled;
    this.audioSystemProperties.inputDevices[activeIndex]
        .noiseCancellationState = nextState;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /** Handle updating active input device style transfer state. */
  setStyleTransferEnabled(enabled: boolean): void {
    if (!this.audioSystemProperties.inputDevices) {
      return;
    }

    const activeIndex = this.audioSystemProperties.inputDevices.findIndex(
        (device: AudioDevice) => device.isActive &&
            device.styleTransferState !== AudioEffectState.kNotSupported);
    if (activeIndex === -1) {
      return;
    }
    const nextState: AudioEffectState =
        enabled ? AudioEffectState.kEnabled : AudioEffectState.kNotEnabled;
    this.audioSystemProperties.inputDevices[activeIndex].styleTransferState =
        nextState;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /** Handle updating active input device force respect ui gains state. */
  setForceRespectUiGainsEnabled(enabled: boolean): void {
    if (!this.audioSystemProperties.inputDevices) {
      return;
    }

    const activeIndex = this.audioSystemProperties.inputDevices.findIndex(
        (device: AudioDevice) => device.isActive);
    if (activeIndex === -1) {
      return;
    }
    const nextState: AudioEffectState =
        enabled ? AudioEffectState.kEnabled : AudioEffectState.kNotEnabled;
    this.audioSystemProperties.inputDevices[activeIndex]
        .forceRespectUiGainsState = nextState;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /** Handle updating hfp mic sr state. */
  setHfpMicSrEnabled(enabled: boolean): void {
    if (!this.audioSystemProperties.inputDevices) {
      return;
    }

    const activeIndex = this.audioSystemProperties.inputDevices.findIndex(
        (device: AudioDevice) => device.isActive &&
            device.hfpMicSrState !== AudioEffectState.kNotSupported);
    if (activeIndex === -1) {
      return;
    }
    const nextState: AudioEffectState =
        enabled ? AudioEffectState.kEnabled : AudioEffectState.kNotEnabled;
    this.audioSystemProperties.inputDevices[activeIndex].hfpMicSrState =
        nextState;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Returns true if hfpMicSrState is enabled on the device with id `deviceId`.
   */
  isHfpMicSrEnabled(deviceId: bigint): boolean {
      const device = this.audioSystemProperties.inputDevices.find(
          (device: AudioDevice) => device.id === deviceId);
      assert(device !== undefined);
      return device.hfpMicSrState === AudioEffectState.kEnabled;
  }

  /**
   * Sets the mute state based on provided value.
   */
  setOutputMuted(muted: boolean): void {
    this.audioSystemProperties.outputMuteState =
        muted ? MuteState.kMutedByUser : MuteState.kNotMuted;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Sets the input device mute state to `kMutedByUser` when true and
   * `kNotMuted` when false.
   */
  setInputMuted(muted: boolean): void {
    const muteState = muted ? MuteState.kMutedByUser : MuteState.kNotMuted;
    this.audioSystemProperties.inputMuteState = muteState;
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Sets the `inputGainPercent` to the desired volume and notifies
   * observers.
   */
  setInputGainPercent(gain: number): void {
    assert(gain >= 0 && gain <= 100);
    this.audioSystemProperties.inputGainPercent = Math.round(gain);
    this.notifyAudioSystemPropertiesUpdated();
  }

  /**
   * Sets the `outputVolumePercent` to the desired volume and notifies
   * observers.
   */
  setOutputVolumePercent(volume: number): void {
    this.audioSystemProperties.outputVolumePercent = Math.round(volume);
    this.notifyAudioSystemPropertiesUpdated();
  }

  /** Notifies the observer list that `audioSystemProperties` has changed. */
  private notifyAudioSystemPropertiesUpdated(): void {
    this.observers.forEach((observer: FakePropertiesObserverInterface) => {
      observer.onPropertiesUpdated(this.audioSystemProperties);
    });
  }
}