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

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

/**
 * @fileoverview 'settings-device-page' is the settings page for device and
 * peripheral settings.
 */
import '/shared/settings/prefs/prefs.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_link_row/cr_link_row.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './audio.js';
import './display.js';
import './graphics_tablet_subpage.js';
import './keyboard.js';
import './per_device_keyboard.js';
import './per_device_keyboard_remap_keys.js';
import './per_device_mouse.js';
import './per_device_pointing_stick.js';
import './per_device_touchpad.js';
import './pointers.js';
import './power.js';
import './storage.js';
import './storage_external.js';
import './stylus.js';
import '../os_settings_page/os_settings_animated_pages.js';
import '../os_settings_page/os_settings_subpage.js';
import '../os_settings_page/settings_card.js';
import '../settings_shared.css.js';
import '../os_printing_page/printing_settings_card.js';

import {getInstance as getAnnouncerInstance} from 'chrome://resources/ash/common/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {isExternalStorageEnabled, isInputDeviceSettingsSplitEnabled, isRevampWayfindingEnabled} from '../common/load_time_booleans.js';
import {RouteOriginMixin} from '../common/route_origin_mixin.js';
import {PrefsState} from '../common/types.js';
import {KeyboardPolicies, MousePolicies} from '../mojom-webui/input_device_settings.mojom-webui.js';
import {GraphicsTabletSettingsObserverReceiver, KeyboardSettingsObserverReceiver, MouseSettingsObserverReceiver, PointingStickSettingsObserverReceiver, TouchpadSettingsObserverReceiver} from '../mojom-webui/input_device_settings_provider.mojom-webui.js';
import {Section} from '../mojom-webui/routes.mojom-webui.js';
import {ACCESSIBILITY_COMMON_IME_ID} from '../os_languages_page/languages.js';
import {LanguageHelper, LanguagesModel} from '../os_languages_page/languages_types.js';
import {Route, Router, routes} from '../router.js';

import {getTemplate} from './device_page.html.js';
import {DevicePageBrowserProxy, DevicePageBrowserProxyImpl} from './device_page_browser_proxy.js';
import {FakeInputDeviceSettingsProvider} from './fake_input_device_settings_provider.js';
import {getInputDeviceSettingsProvider} from './input_device_mojo_interface_provider.js';
import {GraphicsTablet, InputDeviceSettingsProviderInterface, Keyboard, Mouse, PointingStick, Touchpad} from './input_device_settings_types.js';
import {SettingsPerDeviceKeyboardRemapKeysElement} from './per_device_keyboard_remap_keys.js';

const SettingsDevicePageElementBase =
    RouteOriginMixin(I18nMixin(WebUiListenerMixin(PolymerElement)));

export class SettingsDevicePageElement extends SettingsDevicePageElementBase {
  static get is() {
    return 'settings-device-page' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      prefs: {
        type: Object,
        notify: true,
      },

      section_: {
        type: Number,
        value: Section.kDevice,
        readOnly: true,
      },

      /**
       * |hasMouse_|, |hasPointingStick_|, and |hasTouchpad_| start undefined so
       * observers don't trigger until they have been populated.
       */
      hasMouse_: Boolean,

      /**
       * Whether a pointing stick (such as a TrackPoint) is connected.
       */
      hasPointingStick_: Boolean,

      hasTouchpad_: Boolean,

      /**
       * Whether the device has a haptic touchpad. If this is true,
       * |hasTouchpad_| will also be true.
       */
      hasHapticTouchpad_: Boolean,

      /**
       * |hasStylus_| is initialized to false so that dom-if behaves correctly.
       */
      hasStylus_: {
        type: Boolean,
        value: false,
      },

      /**
       * Whether settings should be split per device.
       */
      isDeviceSettingsSplitEnabled_: {
        type: Boolean,
        value() {
          return isInputDeviceSettingsSplitEnabled();
        },
        readOnly: true,
      },

      /**
       * Whether users are allowed to customize buttons on their peripherals.
       */
      isPeripheralCustomizationEnabled: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('enablePeripheralCustomization');
        },
        readOnly: true,
      },

      isRevampWayfindingEnabled_: {
        type: Boolean,
        value: () => {
          return isRevampWayfindingEnabled();
        },
        readOnly: true,
      },

      /**
       * Whether storage management info should be hidden.
       */
      hideStorageInfo_: {
        type: Boolean,
        value() {
          // TODO(crbug.com/40587075): Show an explanatory message instead.
          return loadTimeData.valueExists('isDemoSession') &&
              loadTimeData.getBoolean('isDemoSession');
        },
        readOnly: true,
      },

      isExternalStorageEnabled_: {
        type: Boolean,
        value() {
          return isExternalStorageEnabled();
        },
      },

      pointingSticks: {
        type: Array,
      },

      keyboards: {
        type: Array,
      },

      keyboardPolicies: {
        type: Object,
      },

      touchpads: {
        type: Array,
      },

      mice: {
        type: Array,
      },

      mousePolicies: {
        type: Object,
      },

      graphicsTablets: {
        type: Array,
      },

      /**
       * Set of languages from <settings-languages>
       */
      languages: Object,

      /**
       * Language helper API from <settings-languages>
       */
      languageHelper: Object,

      inputMethodDisplayName_: {
        type: String,
        computed: 'computeInputMethodDisplayName_(' +
            'languages.inputMethods.currentId, languageHelper)',
      },

      rowIcons_: {
        type: Object,
        value() {
          if (isRevampWayfindingEnabled()) {
            return {
              mouse: 'os-settings:device-mouse',
              touchpad: 'os-settings:device-touchpad',
              pointingStick: 'os-settings:device-pointing-stick',
              keyboardAndInputs: 'os-settings:device-keyboard',
              stylus: 'os-settings:device-stylus',
              tablet: 'os-settings:device-tablet',
              display: 'os-settings:device-display',
              audio: 'os-settings:device-audio',
            };
          }

          return {
            mouse: '',
            touchpad: '',
            pointingStick: '',
            keyboardAndInputs: '',
            stylus: '',
            tablet: '',
            display: '',
            audio: '',
          };
        },
      },
    };
  }

  static get observers() {
    return [
      'pointersChanged_(hasMouse_, hasPointingStick_, hasTouchpad_)',
      'mouseChanged_(mice)',
      'touchpadChanged_(touchpads)',
      'pointingStickChanged_(pointingSticks)',
      'graphicsTabletChanged_(graphicsTablets)',
    ];
  }

  languages: LanguagesModel|undefined;
  languageHelper: LanguageHelper|undefined;
  prefs: PrefsState|undefined;

  protected pointingSticks: PointingStick[];
  protected keyboards: Keyboard[];
  protected keyboardPolicies: KeyboardPolicies;
  protected touchpads: Touchpad[];
  protected mice: Mouse[];
  protected mousePolicies: MousePolicies;
  protected graphicsTablets: GraphicsTablet[];
  private browserProxy_: DevicePageBrowserProxy;
  private hasMouse_: boolean;
  private hasPointingStick_: boolean;
  private hasTouchpad_: boolean;
  private hasHapticTouchpad_: boolean;
  private isDeviceSettingsSplitEnabled_: boolean;
  private isPeripheralCustomizationEnabled: boolean;
  private isRevampWayfindingEnabled_: boolean;
  private pointingStickSettingsObserverReceiver:
      PointingStickSettingsObserverReceiver;
  private keyboardSettingsObserverReceiver: KeyboardSettingsObserverReceiver;
  private touchpadSettingsObserverReceiver: TouchpadSettingsObserverReceiver;
  private inputDeviceSettingsProvider: InputDeviceSettingsProviderInterface;
  private mouseSettingsObserverReceiver: MouseSettingsObserverReceiver;
  private graphicsTabletSettingsObserverReceiver:
      GraphicsTabletSettingsObserverReceiver;
  private rowIcons_: Record<string, string>;
  private section_: Section;

  constructor() {
    super();

    /** RouteOriginMixin override */
    this.route = routes.DEVICE;

    this.browserProxy_ = DevicePageBrowserProxyImpl.getInstance();
    if (this.isDeviceSettingsSplitEnabled_) {
      this.inputDeviceSettingsProvider = getInputDeviceSettingsProvider();
      this.observePointingStickSettings();
      this.observeKeyboardSettings();
      this.observeTouchpadSettings();
      this.observeMouseSettings();
      if (this.isPeripheralCustomizationEnabled) {
        // The flag `isPeripheralCustomizationEnabled` should only be enabled
        // when `isDeviceSettingsSplitEnabled_` is enabled. Will not call
        // `getInputDeviceSettingsProvider` here again.
        this.observeGraphicsTabletSettings();
      }
    }

  }

  override connectedCallback(): void {
    super.connectedCallback();

    if (!this.isDeviceSettingsSplitEnabled_) {
      this.addWebUiListener(
          'has-mouse-changed', this.set.bind(this, 'hasMouse_'));
      this.addWebUiListener(
          'has-pointing-stick-changed',
          this.set.bind(this, 'hasPointingStick_'));
      this.addWebUiListener(
          'has-touchpad-changed', this.set.bind(this, 'hasTouchpad_'));
      this.addWebUiListener(
          'has-haptic-touchpad-changed',
          this.set.bind(this, 'hasHapticTouchpad_'));
      this.browserProxy_.initializePointers();
    }

    this.addWebUiListener(
        'has-stylus-changed', this.set.bind(this, 'hasStylus_'));
    this.browserProxy_.initializeStylus();

    this.addWebUiListener(
        'storage-android-enabled-changed',
        this.set.bind(this, 'isExternalStorageEnabled_'));
    this.browserProxy_.updateAndroidEnabled();
  }

  override ready(): void {
    super.ready();

    this.addFocusConfig(routes.POINTERS, '#pointersRow');
    this.addFocusConfig(routes.PER_DEVICE_MOUSE, '#perDeviceMouseRow');
    this.addFocusConfig(routes.PER_DEVICE_TOUCHPAD, '#perDeviceTouchpadRow');
    this.addFocusConfig(
        routes.PER_DEVICE_POINTING_STICK, '#perDevicePointingStickRow');
    this.addFocusConfig(routes.PER_DEVICE_KEYBOARD, '#perDeviceKeyboardRow');
    this.addFocusConfig(
        routes.PER_DEVICE_KEYBOARD_REMAP_KEYS,
        '#perDeviceKeyboardRemapKeysRow');
    this.addFocusConfig(routes.KEYBOARD, '#keyboardRow');
    this.addFocusConfig(routes.STYLUS, '#stylusRow');
    this.addFocusConfig(routes.DISPLAY, '#displayRow');
    this.addFocusConfig(routes.AUDIO, '#audioRow');
    this.addFocusConfig(routes.GRAPHICS_TABLET, '#tabletRow');
    this.addFocusConfig(
        routes.CUSTOMIZE_MOUSE_BUTTONS, '#customizeMouseButtonsRow');
    this.addFocusConfig(
        routes.CUSTOMIZE_TABLET_BUTTONS, '#customizeTabletButtonsSubpage');
    this.addFocusConfig(
        routes.CUSTOMIZE_PEN_BUTTONS, '#customizePenButtonsSubpage');

    if (!this.isRevampWayfindingEnabled_) {
      this.addFocusConfig(routes.STORAGE, '#storageRow');
      this.addFocusConfig(routes.POWER, '#powerRow');
    }
  }

  private observePointingStickSettings(): void {
    if (this.inputDeviceSettingsProvider instanceof
        FakeInputDeviceSettingsProvider) {
      this.inputDeviceSettingsProvider.observePointingStickSettings(this);
      return;
    }

    this.pointingStickSettingsObserverReceiver =
        new PointingStickSettingsObserverReceiver(this);

    this.inputDeviceSettingsProvider.observePointingStickSettings(
        this.pointingStickSettingsObserverReceiver.$
            .bindNewPipeAndPassRemote());
  }

  onPointingStickListUpdated(pointingSticks: PointingStick[]): void {
    this.pointingSticks = pointingSticks;
  }

  private observeKeyboardSettings(): void {
    if (this.inputDeviceSettingsProvider instanceof
        FakeInputDeviceSettingsProvider) {
      this.inputDeviceSettingsProvider.observeKeyboardSettings(this);
      return;
    }

    this.keyboardSettingsObserverReceiver =
        new KeyboardSettingsObserverReceiver(this);

    this.inputDeviceSettingsProvider.observeKeyboardSettings(
        this.keyboardSettingsObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  onKeyboardListUpdated(keyboards: Keyboard[]): void {
    this.keyboards = keyboards;
  }

  onKeyboardPoliciesUpdated(keyboardPolicies: KeyboardPolicies): void {
    this.keyboardPolicies = keyboardPolicies;
  }

  private observeTouchpadSettings(): void {
    if (this.inputDeviceSettingsProvider instanceof
        FakeInputDeviceSettingsProvider) {
      this.inputDeviceSettingsProvider.observeTouchpadSettings(this);
      return;
    }

    this.touchpadSettingsObserverReceiver =
        new TouchpadSettingsObserverReceiver(this);

    this.inputDeviceSettingsProvider.observeTouchpadSettings(
        this.touchpadSettingsObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  onTouchpadListUpdated(touchpads: Touchpad[]): void {
    this.touchpads = touchpads;
  }

  private observeMouseSettings(): void {
    if (this.inputDeviceSettingsProvider instanceof
        FakeInputDeviceSettingsProvider) {
      this.inputDeviceSettingsProvider.observeMouseSettings(this);
      return;
    }

    this.mouseSettingsObserverReceiver =
        new MouseSettingsObserverReceiver(this);

    this.inputDeviceSettingsProvider.observeMouseSettings(
        this.mouseSettingsObserverReceiver.$.bindNewPipeAndPassRemote());
  }

  onMouseListUpdated(mice: Mouse[]): void {
    this.mice = mice;
  }

  onMousePoliciesUpdated(mousePolicies: MousePolicies): void {
    this.mousePolicies = mousePolicies;
  }

  private observeGraphicsTabletSettings(): void {
    if (this.inputDeviceSettingsProvider instanceof
        FakeInputDeviceSettingsProvider) {
      this.inputDeviceSettingsProvider.observeGraphicsTabletSettings(this);
      return;
    }

    this.graphicsTabletSettingsObserverReceiver =
        new GraphicsTabletSettingsObserverReceiver(this);

    this.inputDeviceSettingsProvider.observeGraphicsTabletSettings(
        this.graphicsTabletSettingsObserverReceiver.$
            .bindNewPipeAndPassRemote());
  }

  onGraphicsTabletListUpdated(graphicsTablets: GraphicsTablet[]): void {
    this.graphicsTablets = graphicsTablets;
  }

  private getPointersTitle_(): string {
    // For the purposes of the title, we call pointing sticks mice. The user
    // will know what we mean, and otherwise we'd get too many possible titles.
    const hasMouseOrPointingStick = this.hasMouse_ || this.hasPointingStick_;
    if (hasMouseOrPointingStick && this.hasTouchpad_) {
      return this.i18n('mouseAndTouchpadTitle');
    }
    if (hasMouseOrPointingStick) {
      return this.i18n('mouseTitle');
    }
    if (this.hasTouchpad_) {
      return this.i18n('touchpadTitle');
    }
    return '';
  }

  /**
   * Handler for tapping the mouse and touchpad settings menu item.
   */
  private onPointersClick_(): void {
    Router.getInstance().navigateTo(routes.POINTERS);
  }

  /**
   * Handler for tapping the mouse and touchpad settings menu item.
   */
  private onPerDeviceKeyboardClick_(): void {
    Router.getInstance().navigateTo(routes.PER_DEVICE_KEYBOARD);
  }

  /**
   * Handler for tapping the Mouse settings menu item.
   */
  private onPerDeviceMouseClick_(): void {
    Router.getInstance().navigateTo(routes.PER_DEVICE_MOUSE);
  }

  /**
   * Handler for tapping the Touchpad settings menu item.
   */
  private onPerDeviceTouchpadClick_(): void {
    Router.getInstance().navigateTo(routes.PER_DEVICE_TOUCHPAD);
  }

  /**
   * Handler for tapping the Pointing stick settings menu item.
   */
  private onPerDevicePointingStickClick_(): void {
    Router.getInstance().navigateTo(routes.PER_DEVICE_POINTING_STICK);
  }

  /**
   * Handler for tapping the Keyboard settings menu item.
   */
  private onKeyboardClick_(): void {
    Router.getInstance().navigateTo(routes.KEYBOARD);
  }

  /**
   * Handler for tapping the Stylus settings menu item.
   */
  private onStylusClick_(): void {
    Router.getInstance().navigateTo(routes.STYLUS);
  }

  /**
   * Handler for tapping the Graphics tablet settings menu item.
   */
  private onGraphicsTabletClick(): void {
    Router.getInstance().navigateTo(routes.GRAPHICS_TABLET);
  }

  /**
   * Handler for tapping the Display settings menu item.
   */
  private onDisplayClick_(): void {
    Router.getInstance().navigateTo(routes.DISPLAY);
  }

  /**
   * Handler for tapping the Audio settings menu item.
   */
  private onAudioClick_(): void {
    Router.getInstance().navigateTo(routes.AUDIO);
  }

  /**
   * Handler for tapping the Storage settings menu item.
   */
  private onStorageClick_(): void {
    Router.getInstance().navigateTo(routes.STORAGE);
  }

  /**
   * Handler for tapping the Power settings menu item.
   */
  private onPowerClick_(): void {
    Router.getInstance().navigateTo(routes.POWER);
  }

  override currentRouteChanged(newRoute: Route, oldRoute?: Route): void {
    super.currentRouteChanged(newRoute, oldRoute);

    this.checkPointerSubpage_();
  }

  private pointersChanged_(): void {
    this.checkPointerSubpage_();
  }

  private mouseChanged_(): void {
    if ((!this.mice || this.mice.length === 0) &&
        Router.getInstance().currentRoute === routes.PER_DEVICE_MOUSE) {
      getAnnouncerInstance().announce(
          this.i18n('allMiceDisconnectedA11yLabel'));
      Router.getInstance().navigateTo(routes.DEVICE);
    }
  }

  private touchpadChanged_(): void {
    if ((!this.touchpads || this.touchpads.length === 0) &&
        Router.getInstance().currentRoute === routes.PER_DEVICE_TOUCHPAD) {
      getAnnouncerInstance().announce(
          this.i18n('allTouchpadsDisconnectedA11yLabel'));

      Router.getInstance().navigateTo(routes.DEVICE);
    }
  }

  private pointingStickChanged_(): void {
    if ((!this.pointingSticks || this.pointingSticks.length === 0) &&
        Router.getInstance().currentRoute ===
            routes.PER_DEVICE_POINTING_STICK) {
      getAnnouncerInstance().announce(
          this.i18n('allPointingSticksDisconnectedA11yLabel'));
      Router.getInstance().navigateTo(routes.DEVICE);
    }
  }

  private graphicsTabletChanged_(): void {
    if ((!this.graphicsTablets || this.graphicsTablets.length === 0) &&
        Router.getInstance().currentRoute === routes.GRAPHICS_TABLET) {
      getAnnouncerInstance().announce(
          this.i18n('allGraphicsTabletsDisconnectedA11yLabel'));
      Router.getInstance().navigateTo(routes.DEVICE);
    }
  }

  private showPointersRow_(): boolean {
    return (this.hasMouse_ || this.hasTouchpad_ || this.hasPointingStick_) &&
        !this.isDeviceSettingsSplitEnabled_;
  }

  private showPerDeviceMouseRow_(): boolean {
    return this.isDeviceSettingsSplitEnabled_ && this.mice &&
        this.mice.length !== 0;
  }

  private showPerDeviceTouchpadRow_(touchpads: Touchpad[]): boolean {
    return this.isDeviceSettingsSplitEnabled_ && touchpads &&
        touchpads.length !== 0;
  }

  private showPerDevicePointingStickRow_(): boolean {
    return this.isDeviceSettingsSplitEnabled_ && this.pointingSticks &&
        this.pointingSticks.length !== 0;
  }

  private showGraphicsTabletRow_(): boolean {
    return this.isPeripheralCustomizationEnabled && this.graphicsTablets &&
        this.graphicsTablets.length !== 0;
  }

  protected restoreDefaults(): void {
    const remapKeysPage =
        this.shadowRoot!
            .querySelector<SettingsPerDeviceKeyboardRemapKeysElement>(
                '#remap-keys')!;
    remapKeysPage.restoreDefaults();
  }
  /**
   * Leaves the pointer subpage if all pointing devices are detached.
   */
  private checkPointerSubpage_(): void {
    // Check that the properties have explicitly been set to false.
    if (this.hasMouse_ === false && this.hasPointingStick_ === false &&
        this.hasTouchpad_ === false &&
        Router.getInstance().currentRoute === routes.POINTERS) {
      Router.getInstance().navigateTo(routes.DEVICE);
    }
  }

  /**
   * Computes the display name for the currently configured input method. This
   * should be displayed as a sublabel under the Keyboard and inputs row, only
   * when OsSettingsRevampWayfinding is enabled.
   */
  private computeInputMethodDisplayName_(): string {
    if (!this.isRevampWayfindingEnabled_) {
      return '';
    }

    const id = this.languages?.inputMethods?.currentId;
    if (!id || !this.languageHelper) {
      return '';
    }
    if (id === ACCESSIBILITY_COMMON_IME_ID) {
      return '';
    }
    return this.languageHelper.getInputMethodDisplayName(id);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [SettingsDevicePageElement.is]: SettingsDevicePageElement;
  }
}

customElements.define(SettingsDevicePageElement.is, SettingsDevicePageElement);