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

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

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

import {ActionChoice, Button, ButtonPressObserverInterface, GraphicsTablet, GraphicsTabletObserverInterface, GraphicsTabletSettings, InputDeviceSettingsProviderInterface, Keyboard, KeyboardAmbientLightSensorObserverInterface, KeyboardBrightnessObserverInterface, KeyboardObserverInterface, KeyboardSettings, LidStateObserverInterface, MetaKey, ModifierKey, Mouse, MouseObserverInterface, MouseSettings, PointingStick, PointingStickObserverInterface, PointingStickSettings, SixPackShortcutModifier, Stylus, StylusObserverInterface, Touchpad, TouchpadObserverInterface, TouchpadSettings} from './input_device_settings_types.js';

/**
 * @fileoverview
 * Implements a fake version of the FakePerDeviceKeyboardProvider mojo
 * interface.
 */

interface InputDeviceSettingsType {
  fakeKeyboards: Keyboard[];
  fakeTouchpads: Touchpad[];
  fakeMice: Mouse[];
  fakePointingSticks: PointingStick[];
  fakeStyluses: Stylus[];
  fakeGraphicsTablets: GraphicsTablet[];
  fakeMouseButtonActions: {options: ActionChoice[]};
  fakeGraphicsTabletButtonActions: {options: ActionChoice[]};
  fakeMetaKeyToDisplay: {metaKey: MetaKey};
  fakeDeviceIconImage: {dataUrl: string|null};
  fakeHasKeyboardBacklight: {hasKeyboardBacklight: boolean};
  fakeHasAmbientLightSensor: {hasAmbientLightSensor: boolean};
  fakeIsRgbKeyboardSupported: {isRgbKeyboardSupported: boolean};
}

class FakeMethodState {
  private result = undefined;

  resolveMethod(): Promise<any> {
    const promise = new Promise((resolve) => {
      resolve(this.result);
    });
    return promise;
  }

  getResult(): any {
    return this.result;
  }

  setResult(result: any): void {
    this.result = result;
  }
}

/**
 * Manages a map of fake async methods, their resolvers and the fake
 * return value they will produce.
 */
export class FakeMethodResolver {
  private methodMap: Map<string, FakeMethodState> = new Map();

  register(methodName: string): void {
    this.methodMap.set(methodName, new FakeMethodState());
  }

  getResult<K extends keyof InputDeviceSettingsType, T>(methodName: K):
      InputDeviceSettingsType[K] extends T? InputDeviceSettingsType[K]: never {
    return this.getState(methodName).getResult();
  }

  setResult<K extends keyof InputDeviceSettingsType, T>(
      methodName: K, result: InputDeviceSettingsType[K] extends T?
      InputDeviceSettingsType[K]: never): void {
    this.getState(methodName).setResult(result);
  }

  resolveMethod<T extends keyof InputDeviceSettingsType>(methodName: T):
      Promise<InputDeviceSettingsType[T]> {
    return this.getState(methodName).resolveMethod();
  }

  getState(methodName: string): FakeMethodState {
    const state = this.methodMap.get(methodName);
    assert(!!state, `Method '${methodName}' not found.`);
    return state;
  }
}

export class FakeInputDeviceSettingsProvider implements
    InputDeviceSettingsProviderInterface {
  private methods: FakeMethodResolver = new FakeMethodResolver();
  private keyboardObservers: KeyboardObserverInterface[] = [];
  private pointingStickObservers: PointingStickObserverInterface[] = [];
  private mouseObservers: MouseObserverInterface[] = [];
  private touchpadObservers: TouchpadObserverInterface[] = [];
  private stylusObservers: StylusObserverInterface[] = [];
  private graphicsTabletObservers: GraphicsTabletObserverInterface[] = [];
  private buttonPressObservers: ButtonPressObserverInterface[] = [];
  private keyboardBrightnessObserver: KeyboardBrightnessObserverInterface|null =
      null;
  private keyboardAmbientLightSensorObserver:
      KeyboardAmbientLightSensorObserverInterface|null = null;
  private lidStateObserver: LidStateObserverInterface|null = null;
  private observedIds: number[] = [];
  private keyboardBrightness: number = 40.0;
  private keyboardAmbientLightSensorEnabled: boolean = false;
  private keyboardColorLinkClicks: number = 0;
  private isLidOpen: boolean = false;
  private callCounts_ = {
    setGraphicsTabletSettings: 0,
    setMouseSettings: 0,
    recordKeyboardBrightnessChangeFromSlider: 0,
  };

  constructor() {
    // Setup method resolvers.
    this.methods.register('fakeKeyboards');
    this.methods.register('fakeTouchpads');
    this.methods.register('fakeMice');
    this.methods.register('fakePointingSticks');
    this.methods.register('fakeStyluses');
    this.methods.register('fakeGraphicsTablets');
    this.methods.register('fakeMouseButtonActions');
    this.methods.register('fakeGraphicsTabletButtonActions');
    this.methods.register('fakeMetaKeyToDisplay');
    this.methods.register('fakeDeviceIconImage');
    this.methods.register('fakeHasKeyboardBacklight');
    this.methods.register('fakeHasAmbientLightSensor');
    this.methods.register('fakeIsRgbKeyboardSupported');
    this.methods.register('fakeRecordKeyboardColorLinkClicked');
    this.methods.register('fakeRecordKeyboardBrightnessChangeFromSlider');
  }

  setFakeKeyboards(keyboards: Keyboard[]): void {
    this.methods.setResult('fakeKeyboards', keyboards);
    this.notifyKeboardListUpdated();
  }

  getConnectedKeyboardSettings(): Promise<Keyboard[]> {
    return this.methods.resolveMethod('fakeKeyboards');
  }

  setFakeTouchpads(touchpads: Touchpad[]): void {
    this.methods.setResult('fakeTouchpads', touchpads);
    this.notifyTouchpadListUpdated();
  }

  getConnectedTouchpadSettings(): Promise<Touchpad[]> {
    return this.methods.resolveMethod('fakeTouchpads');
  }

  setFakeMice(mice: Mouse[]): void {
    this.methods.setResult('fakeMice', mice);
    this.notifyMouseListUpdated();
  }

  getConnectedMouseSettings(): Promise<Mouse[]> {
    return this.methods.resolveMethod('fakeMice');
  }

  setFakePointingSticks(pointingSticks: PointingStick[]): void {
    this.methods.setResult('fakePointingSticks', pointingSticks);
    this.notifyPointingStickListUpdated();
  }

  getConnectedPointingStickSettings(): Promise<PointingStick[]> {
    return this.methods.resolveMethod('fakePointingSticks');
  }

  setFakeStyluses(styluses: Stylus[]): void {
    this.methods.setResult('fakeStyluses', styluses);
  }

  getConnectedStylusSettings(): Promise<Stylus[]> {
    return this.methods.resolveMethod('fakeStyluses');
  }

  setFakeGraphicsTablets(graphicsTablets: GraphicsTablet[]): void {
    this.methods.setResult('fakeGraphicsTablets', graphicsTablets);
    this.notifyGraphicsTabletListUpdated();
  }

  getConnectedGraphicsTabletSettings(): Promise<GraphicsTablet[]> {
    return this.methods.resolveMethod('fakeGraphicsTablets');
  }

  restoreDefaultKeyboardRemappings(id: number): void {
    const keyboards = this.methods.getResult('fakeKeyboards');
    for (const keyboard of keyboards) {
      if (keyboard.id === id) {
        keyboard.settings.modifierRemappings =
            keyboard.metaKey === MetaKey.kCommand ? {
              [ModifierKey.kControl]: ModifierKey.kMeta,
              [ModifierKey.kMeta]: ModifierKey.kControl,
            } :
                                                    {};
        keyboard.settings.sixPackKeyRemappings = {
          pageDown: SixPackShortcutModifier.kSearch,
          pageUp: SixPackShortcutModifier.kSearch,
          del: SixPackShortcutModifier.kSearch,
          insert: SixPackShortcutModifier.kSearch,
          home: SixPackShortcutModifier.kSearch,
          end: SixPackShortcutModifier.kSearch,
        };
      }
    }
    this.methods.setResult('fakeKeyboards', keyboards);
    this.notifyKeboardListUpdated();
  }

  setKeyboardSettings(id: number, settings: KeyboardSettings): void {
    const keyboards = this.methods.getResult('fakeKeyboards');
    for (const keyboard of keyboards) {
      if (keyboard.id === id) {
        keyboard.settings = settings;
      }
    }
    this.methods.setResult('fakeKeyboards', keyboards);
  }

  setMouseSettings(id: number, settings: MouseSettings): void {
    const mice = this.methods.getResult('fakeMice');
    for (const mouse of mice) {
      if (mouse.id === id) {
        mouse.settings = settings;
      }
    }
    this.methods.setResult('fakeMice', mice);
    this.notifyMouseListUpdated();
    this.callCounts_.setMouseSettings++;
  }

  getSetMouseSettingsCallCount(): number {
    return this.callCounts_.setMouseSettings;
  }

  setTouchpadSettings(id: number, settings: TouchpadSettings): void {
    const touchpads = this.methods.getResult('fakeTouchpads');
    for (const touchpad of touchpads) {
      if (touchpad.id === id) {
        touchpad.settings = settings;
      }
    }
    this.methods.setResult('fakeTouchpads', touchpads);
  }

  setPointingStickSettings(id: number, settings: PointingStickSettings): void {
    const pointingSticks = this.methods.getResult('fakePointingSticks');
    for (const pointingStick of pointingSticks) {
      if (pointingStick.id === id) {
        pointingStick.settings = settings;
      }
    }
    this.methods.setResult('fakePointingSticks', pointingSticks);
  }

  setGraphicsTabletSettings(id: number, settings: GraphicsTabletSettings):
      void {
    const graphicsTablets = this.methods.getResult('fakeGraphicsTablets');
    for (const graphicsTablet of graphicsTablets) {
      if (graphicsTablet.id === id) {
        graphicsTablet.settings = settings;
      }
    }
    this.methods.setResult('fakeGraphicsTablets', graphicsTablets);
    this.notifyGraphicsTabletListUpdated();
    this.callCounts_.setGraphicsTabletSettings++;
  }

  getSetGraphicsTabletSettingsCallCount(): number {
    return this.callCounts_.setGraphicsTabletSettings;
  }

  setKeyboardBrightness(percent: number): void {
    this.keyboardBrightness = percent;
  }

  getKeyboardBrightness(): number {
    return this.keyboardBrightness;
  }

  setKeyboardAmbientLightSensorEnabled(enabled: boolean): void {
    this.keyboardAmbientLightSensorEnabled = enabled;
  }

  getKeyboardAmbientLightSensorEnabled(): boolean {
    return this.keyboardAmbientLightSensorEnabled;
  }


  notifyKeboardListUpdated(): void {
    const keyboards = this.methods.getResult('fakeKeyboards');
    // Make a deep copy to notify the functions observing keyboard settings.
    const keyboardsClone = !keyboards ? keyboards : structuredClone(keyboards);
    for (const observer of this.keyboardObservers) {
      observer.onKeyboardListUpdated(keyboardsClone);
    }
  }

  notifyTouchpadListUpdated(): void {
    const touchpads = this.methods.getResult('fakeTouchpads');
    for (const observer of this.touchpadObservers) {
      observer.onTouchpadListUpdated(touchpads);
    }
  }

  notifyMouseListUpdated(): void {
    const mice = this.methods.getResult('fakeMice');
    for (const observer of this.mouseObservers) {
      observer.onMouseListUpdated(mice);
    }
  }

  notifyPointingStickListUpdated(): void {
    const pointingSticks = this.methods.getResult('fakePointingSticks');
    for (const observer of this.pointingStickObservers) {
      observer.onPointingStickListUpdated(pointingSticks);
    }
  }

  notifyStylusListUpdated(): void {
    const styluses = this.methods.getResult('fakeStyluses');
    for (const observer of this.stylusObservers) {
      observer.onStylusListUpdated(styluses);
    }
  }

  notifyGraphicsTabletListUpdated(): void {
    const graphicsTablets = this.methods.getResult('fakeGraphicsTablets');
    for (const observer of this.graphicsTabletObservers) {
      observer.onGraphicsTabletListUpdated(graphicsTablets);
    }
  }

  observeKeyboardSettings(observer: KeyboardObserverInterface): void {
    this.keyboardObservers.push(observer);
    this.notifyKeboardListUpdated();
  }

  observeTouchpadSettings(observer: TouchpadObserverInterface): void {
    this.touchpadObservers.push(observer);
    this.notifyTouchpadListUpdated();
  }

  observeMouseSettings(observer: MouseObserverInterface): void {
    this.mouseObservers.push(observer);
    this.notifyMouseListUpdated();
  }

  observePointingStickSettings(observer: PointingStickObserverInterface): void {
    this.pointingStickObservers.push(observer);
    this.notifyPointingStickListUpdated();
  }

  observeStylusSettings(observer: StylusObserverInterface): void {
    this.stylusObservers.push(observer);
    this.notifyStylusListUpdated();
  }

  observeGraphicsTabletSettings(observer: GraphicsTabletObserverInterface):
      void {
    this.graphicsTabletObservers.push(observer);
    this.notifyGraphicsTabletListUpdated();
  }

  observeButtonPresses(observer: ButtonPressObserverInterface): void {
    this.buttonPressObservers.push(observer);
  }

  observeKeyboardBrightness(observer: KeyboardBrightnessObserverInterface):
      void {
    this.keyboardBrightnessObserver = observer;
  }

  observeKeyboardAmbientLightSensor(
      observer: KeyboardAmbientLightSensorObserverInterface): void {
    this.keyboardAmbientLightSensorObserver = observer;
  }

  observeLidState(observer: LidStateObserverInterface):
      Promise<{isLidOpen: boolean}> {
    this.lidStateObserver = observer;
    return Promise.resolve({isLidOpen: true});
  }

  setLidStateOpen(): void {
    this.lidStateObserver!.onLidStateChanged(true);
  }

  setLidStateClosed(): void {
    this.lidStateObserver!.onLidStateChanged(false);
  }

  getActionsForMouseButtonCustomization(): Promise<{options: ActionChoice[]}> {
    return this.methods.resolveMethod('fakeMouseButtonActions');
  }

  setFakeActionsForMouseButtonCustomization(actionChoices: ActionChoice[]):
      void {
    this.methods.setResult('fakeMouseButtonActions', {options: actionChoices});
  }

  getActionsForGraphicsTabletButtonCustomization():
      Promise<{options: ActionChoice[]}> {
    return this.methods.resolveMethod('fakeGraphicsTabletButtonActions');
  }

  setDeviceIconImage(dataUrl: string): void {
    return this.methods.setResult('fakeDeviceIconImage', {dataUrl});
  }

  getDeviceIconImage(): Promise<{dataUrl: string | null}> {
    return this.methods.resolveMethod('fakeDeviceIconImage');
  }

  launchCompanionApp(): void {}

  setFakeActionsForGraphicsTabletButtonCustomization(actionChoices:
                                                         ActionChoice[]): void {
    this.methods.setResult(
        'fakeGraphicsTabletButtonActions', {options: actionChoices});
  }

  startObserving(id: number): void {
    if (this.observedIds.includes(id)) {
      return;
    }
    this.observedIds.push(id);
  }

  stopObserving(): void {
    this.observedIds = [];
  }

  getObservedDevices(): number[] {
    return this.observedIds;
  }

  sendButtonPress(button: Button): void {
    for (const observer of this.buttonPressObservers) {
      observer.onButtonPressed(button);
    }
  }

  sendKeyboardBrightnessChange(percent: number): void {
    if (this.keyboardBrightnessObserver) {
      this.keyboardBrightnessObserver.onKeyboardBrightnessChanged(percent);
    }
  }

  sendKeyboardAmbientLightSensorEnabledChange(enabled: boolean): void {
    if (this.keyboardAmbientLightSensorObserver) {
      this.keyboardAmbientLightSensorObserver
          .onKeyboardAmbientLightSensorEnabledChanged(enabled);
    }
  }

  getMetaKeyToDisplay(): Promise<{metaKey: MetaKey}> {
    return this.methods.resolveMethod('fakeMetaKeyToDisplay');
  }

  setFakeMetaKeyToDisplay(metaKey: MetaKey): void {
    this.methods.setResult('fakeMetaKeyToDisplay', {metaKey: metaKey});
  }

  hasKeyboardBacklight(): Promise<{hasKeyboardBacklight: boolean}> {
    return this.methods.resolveMethod('fakeHasKeyboardBacklight');
  }

  setFakeHasKeyboardBacklight(hasKeyboardBacklight: boolean): void {
    this.methods.setResult(
        'fakeHasKeyboardBacklight',
        {hasKeyboardBacklight: hasKeyboardBacklight});
  }

  hasAmbientLightSensor(): Promise<{hasAmbientLightSensor: boolean}> {
    return this.methods.resolveMethod('fakeHasAmbientLightSensor');
  }

  setFakeHasAmbientLightSensor(hasAmbientLightSensor: boolean): void {
    this.methods.setResult(
        'fakeHasAmbientLightSensor',
        {hasAmbientLightSensor: hasAmbientLightSensor});
  }

  isRgbKeyboardSupported(): Promise<{isRgbKeyboardSupported: boolean}> {
    return this.methods.resolveMethod('fakeIsRgbKeyboardSupported');
  }

  setFakeIsRgbKeyboardSupported(isRgbKeyboardSupported: boolean): void {
    this.methods.setResult(
        'fakeIsRgbKeyboardSupported',
        {isRgbKeyboardSupported: isRgbKeyboardSupported});
  }

  recordKeyboardColorLinkClicked(): void {
    this.keyboardColorLinkClicks++;
  }

  getKeyboardColorLinkClicks(): number {
    return this.keyboardColorLinkClicks;
  }

  recordKeyboardBrightnessChangeFromSlider(percent: number): void {
    assert(percent >= 0);
    this.callCounts_.recordKeyboardBrightnessChangeFromSlider++;
  }

  getRecordKeyboardBrightnessChangeFromSliderCallCount(): number {
    return this.callCounts_.recordKeyboardBrightnessChangeFromSlider;
  }
}