chromium/chrome/browser/resources/ash/settings/os_bluetooth_page/os_bluetooth_summary.ts

// Copyright 2021 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 subpage providing high level summary of the state of Bluetooth and
 * its connected devices.
 */

import '../settings_shared.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';

import {getDeviceNameUnsafe} from 'chrome://resources/ash/common/bluetooth/bluetooth_utils.js';
import {getBluetoothConfig} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {getHidPreservingController} from 'chrome://resources/ash/common/bluetooth/hid_preserving_bluetooth_state_controller.js';
import {HidWarningDialogSource} from 'chrome://resources/ash/common/bluetooth/hid_preserving_bluetooth_state_controller.mojom-webui.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 {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, PairedBluetoothDeviceProperties} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {RouteOriginMixin} from '../common/route_origin_mixin.js';
import {Route, Router, routes} from '../router.js';

import {OsBluetoothDevicesSubpageBrowserProxy, OsBluetoothDevicesSubpageBrowserProxyImpl} from './os_bluetooth_devices_subpage_browser_proxy.js';
import {getTemplate} from './os_bluetooth_summary.html.js';

/**
 * Refers to Bluetooth secondary text label, used to distinguish between
 * accessibility string and UI text string.
 */
enum LabelType {
  A11Y = 1,
  DISPLAYED_TEXT = 2,
}

const SettingsBluetoothSummaryElementBase =
    RouteOriginMixin(I18nMixin(PolymerElement));

export class SettingsBluetoothSummaryElement extends
    SettingsBluetoothSummaryElementBase {
  static get is() {
    return 'os-settings-bluetooth-summary' as const;
  }

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

  static get properties() {
    return {
      systemProperties: {
        type: Object,
        observer: 'onSystemPropertiesChanged_',
      },

      /**
       * Reflects the current state of the toggle button. This will be set when
       * the |systemProperties| state changes or when the user presses the
       * toggle.
       */
      isBluetoothToggleOn_: {
        type: Boolean,
        observer: 'onIsBluetoothToggleOnChanged_',
      },

      LabelType: {
        type: Object,
        value: LabelType,
      },

      /**
       * Whether the user is a secondary user.
       */
      isSecondaryUser_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('isSecondaryUser');
        },
        readOnly: true,
      },

      /**
       * Email address for the primary user.
       */
      primaryUserEmail_: {
        type: String,
        value() {
          return loadTimeData.getString('primaryUserEmail');
        },
        readOnly: true,
      },

      isBluetoothDisconnectWarningEnabled_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('bluetoothDisconnectWarningFlag');
        },
        readOnly: true,
      },
    };
  }

  /* eslint-disable-next-line @typescript-eslint/naming-convention */
  LabelType: LabelType;
  systemProperties: BluetoothSystemProperties;
  private browserProxy_: OsBluetoothDevicesSubpageBrowserProxy;
  private isBluetoothToggleOn_: boolean;
  private isSecondaryUser_: boolean;
  private primaryUserEmail_: string;
  private isBluetoothDisconnectWarningEnabled_: boolean;

  constructor() {
    super();

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

    this.browserProxy_ =
        OsBluetoothDevicesSubpageBrowserProxyImpl.getInstance();
  }

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

    this.addFocusConfig(routes.BLUETOOTH_DEVICES, '.subpage-arrow');
  }

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

    if (newRoute === this.route) {
      this.browserProxy_.showBluetoothRevampHatsSurvey();
    }
  }

  private onSystemPropertiesChanged_(): void {
    this.isBluetoothToggleOn_ =
        this.systemProperties.systemState === BluetoothSystemState.kEnabled ||
        this.systemProperties.systemState === BluetoothSystemState.kEnabling;
  }

  /**
   * Observer for isBluetoothToggleOn_ that returns early until the previous
   * value was not undefined to avoid wrongly toggling the Bluetooth state.
   */
  private onIsBluetoothToggleOnChanged_(_newValue: boolean, oldValue?: boolean):
      void {
    if (oldValue === undefined) {
      return;
    }

    getAnnouncerInstance().announce(
        this.isBluetoothToggleOn_ ? this.i18n('bluetoothEnabledA11YLabel') :
                                    this.i18n('bluetoothDisabledA11YLabel'));
  }

  private isToggleDisabled_(): boolean {
    if (!this.systemProperties) {
      return false;
    }
    // TODO(crbug.com/1010321): Add check for modification state when variable
    // is available.
    return this.systemProperties.systemState ===
        BluetoothSystemState.kUnavailable;
  }

  private getSecondaryLabel_(labelType: LabelType): string {
    if (!this.isBluetoothToggleOn_) {
      return this.i18n('bluetoothSummaryPageOff');
    }

    const connectedDevices = this.getConnectedDevices_();

    if (!connectedDevices.length) {
      return this.i18n('bluetoothSummaryPageOn');
    }

    const isA11yLabel = labelType === LabelType.A11Y;
    const firstConnectedDeviceName = getDeviceNameUnsafe(connectedDevices[0]);

    if (connectedDevices.length === 1) {
      return isA11yLabel ? loadTimeData.getStringF(
                               'bluetoothSummaryPageConnectedA11yOneDevice',
                               firstConnectedDeviceName) :
                           firstConnectedDeviceName;
    }

    if (connectedDevices.length === 2) {
      const secondConnectedDeviceName =
          getDeviceNameUnsafe(connectedDevices[1]);
      return isA11yLabel ?
          loadTimeData.getStringF(
              'bluetoothSummaryPageConnectedA11yTwoDevices',
              firstConnectedDeviceName, secondConnectedDeviceName) :
          loadTimeData.getStringF(
              'bluetoothSummaryPageTwoDevicesDescription',
              firstConnectedDeviceName, secondConnectedDeviceName);
    }

    return isA11yLabel ?
        loadTimeData.getStringF(
            'bluetoothSummaryPageConnectedA11yTwoOrMoreDevices',
            firstConnectedDeviceName, connectedDevices.length - 1) :
        loadTimeData.getStringF(
            'bluetoothSummaryPageTwoOrMoreDevicesDescription',
            firstConnectedDeviceName, connectedDevices.length - 1);
  }

  private getConnectedDevices_(): PairedBluetoothDeviceProperties[] {
    const pairedDevices = this.systemProperties.pairedDevices;
    if (!pairedDevices) {
      return [];
    }

    return pairedDevices.filter(
        device => device.deviceProperties.connectionState ===
            DeviceConnectionState.kConnected);
  }

  private getBluetoothStatusIconName_(): string {
    if (!this.isBluetoothToggleOn_) {
      return 'os-settings:bluetooth-disabled';
    }

    if (this.getConnectedDevices_().length) {
      return 'os-settings:bluetooth-connected';
    }
    return 'cr:bluetooth';
  }

  private shouldShowSubpageArrow_(): boolean {
    if (this.isToggleDisabled_()) {
      return false;
    }

    return this.isBluetoothToggleOn_;
  }

  private onSubpageArrowClick_(e: Event): void {
    this.navigateToBluetoothDevicesSubpage_();
    e.stopPropagation();
  }

  private navigateToBluetoothDevicesSubpage_(): void {
    Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICES);
  }

  private onWrapperClick_(): void {
    if (this.isToggleDisabled_()) {
      return;
    }

    if (this.systemProperties.systemState === BluetoothSystemState.kDisabled ||
        this.systemProperties.systemState === BluetoothSystemState.kDisabling) {
      this.updateBluetoothState_(true);
      return;
    }
    this.navigateToBluetoothDevicesSubpage_();
  }

  private onPairNewDeviceBtnClick_(): void {
    this.dispatchEvent(new CustomEvent('start-pairing', {
      bubbles: true,
      composed: true,
    }));
  }

  private onBluetoothToggleChange_(event: CustomEvent): void {
    event.stopPropagation();

    // If the toggle value changed but the toggle is disabled, the change came
    // from CrosBluetoothConfig, not the user. Don't attempt to update the
    // enabled state.
    if (this.isToggleDisabled_()) {
      return;
    }
    this.updateBluetoothState_(event.detail);
  }

  private updateBluetoothState_(enabled: boolean): void {
    if (this.isBluetoothDisconnectWarningEnabled_) {
      // Reset Bluetooth toggle state to previous state. Toggle should only be
      // updated when System properties changes.
      this.isBluetoothToggleOn_ = !enabled;
      getHidPreservingController().tryToSetBluetoothEnabledState(
          enabled, HidWarningDialogSource.kOsSettings);
    } else {
      getBluetoothConfig().setBluetoothEnabledState(enabled);
    }

    this.browserProxy_.showBluetoothRevampHatsSurvey();
  }

  private shouldShowPairNewDevice_(): boolean {
    if (!this.systemProperties) {
      return false;
    }

    return this.systemProperties.systemState === BluetoothSystemState.kEnabled;
  }
}

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

customElements.define(
    SettingsBluetoothSummaryElement.is, SettingsBluetoothSummaryElement);