chromium/chrome/browser/resources/ash/settings/os_bluetooth_page/os_bluetooth_devices_subpage.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 for managing Bluetooth properties and devices.
 */

import '../settings_shared.css.js';
import './os_paired_bluetooth_list.js';
import './settings_fast_pair_toggle.js';

import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {BluetoothUiSurface, recordBluetoothUiSurfaceMetrics} from 'chrome://resources/ash/common/bluetooth/bluetooth_metrics_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 {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_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 {DeepLinkingMixin} from '../common/deep_linking_mixin.js';
import {RouteObserverMixin} from '../common/route_observer_mixin.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, Router, routes} from '../router.js';

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

const SettingsBluetoothDevicesSubpageElementBase = DeepLinkingMixin(PrefsMixin(
    RouteObserverMixin(WebUiListenerMixin(I18nMixin(PolymerElement)))));

export class SettingsBluetoothDevicesSubpageElement extends
    SettingsBluetoothDevicesSubpageElementBase {
  static get is() {
    return 'os-settings-bluetooth-devices-subpage' as const;
  }

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

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

      /**
       * Used by DeepLinkingMixin to focus this page's deep links.
       */
      supportedSettingIds: {
        type: Object,
        value: () =>
            new Set<Setting>([Setting.kBluetoothOnOff, Setting.kFastPairOnOff]),
      },

      /**
       * 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_',
      },

      /**
       * Whether or not this device has the requirements to support fast pair.
       */
      isFastPairSupportedByDevice_: {
        type: Boolean,
        value: true,
      },

      connectedDevices_: {
        type: Array,
        value: [],
      },

      savedDevicesSublabel_: {
        type: String,
        value() {
          return loadTimeData.getString('sublabelWithEmail');
        },
      },

      unconnectedDevices_: {
        type: Array,
        value: [],
      },

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

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

      // Consistent with enum `SoftwareScanningStatus` in
      // scanning_enabled_provider.h.
      menuOptions_: {
        type: Array,
        readOnly: true,
        value() {
          return [
            {name: 'Never', value: 0},
            {name: 'Only when charging', value: 2},
          ];
        },
      },

      isBatterySaverActive_: {
        type: Boolean,
        value: false,
      },

      isHardwareOffloadingSupported_: {
        type: Boolean,
        value: false,
      },
    };
  }

  systemProperties: BluetoothSystemProperties;
  private browserProxy_: OsBluetoothDevicesSubpageBrowserProxy;
  private connectedDevices_: PairedBluetoothDeviceProperties[];
  private isBluetoothToggleOn_: boolean;
  private isFastPairSupportedByDevice_: boolean;
  private lastSelectedDeviceId_: string|null;
  private readonly menuOptions_: string[];
  private savedDevicesSublabel_: string;
  private unconnectedDevices_: PairedBluetoothDeviceProperties[];
  private isBluetoothDisconnectWarningEnabled_: boolean;
  private readonly isFastPairSoftwareScanningSupportEnabled_: boolean;
  private isBatterySaverActive_: boolean;
  private isHardwareOffloadingSupported_: boolean;

  constructor() {
    super();

    /**
     * The id of the last device that was selected to view its detail page.
     */
    this.lastSelectedDeviceId_ = null;

    this.browserProxy_ =
        OsBluetoothDevicesSubpageBrowserProxyImpl.getInstance();
  }

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

    if (loadTimeData.getBoolean('enableFastPairFlag')) {
      this.addWebUiListener(
          'fast-pair-device-supported-status', (isSupported: boolean) => {
            this.isFastPairSupportedByDevice_ = isSupported;
          });
      this.browserProxy_.requestFastPairDeviceSupport();
    }

    if (loadTimeData.getBoolean('isFastPairSoftwareScanningSupportEnabled')) {
      // Listen for changes in Battery Saver status.
      this.addWebUiListener(
          'fast-pair-software-scanning-battery-saver-status',
          (isBatterySaverActive: boolean) => {
            this.isBatterySaverActive_ = isBatterySaverActive;
          });
      this.browserProxy_.requestBatterySaverStatus();

      // Listen for changes in Hardware Offloading Support status.
      this.addWebUiListener(
          'fast-pair-software-scanning-hardware-offloading-status',
          (isHardwareOffloadingSupported: boolean) => {
            this.isHardwareOffloadingSupported_ = isHardwareOffloadingSupported;
          });
      this.browserProxy_.requestHardwareOffloadingSupportStatus();
    }
  }

  /**
   * RouteObserverMixin override
   */
  override currentRouteChanged(route: Route, oldRoute?: Route): void {
    // If we're navigating to a device's detail page, save the id of the device.
    if (route === routes.BLUETOOTH_DEVICE_DETAIL &&
        oldRoute === routes.BLUETOOTH_DEVICES) {
      const queryParams = Router.getInstance().getQueryParameters();
      this.lastSelectedDeviceId_ = queryParams.get('id');
      return;
    }

    if (route !== routes.BLUETOOTH_DEVICES) {
      return;
    }
    recordBluetoothUiSurfaceMetrics(
        BluetoothUiSurface.SETTINGS_DEVICE_LIST_SUBPAGE);
    this.browserProxy_.showBluetoothRevampHatsSurvey();

    this.attemptDeepLink();

    // If a backwards navigation occurred from a Bluetooth device's detail page,
    // focus the list item corresponding to that device.
    if (oldRoute !== routes.BLUETOOTH_DEVICE_DETAIL) {
      return;
    }

    // Don't attempt to focus any item unless the last navigation was a
    // 'pop' (backwards) navigation.
    if (!Router.getInstance().lastRouteChangeWasPopstate()) {
      return;
    }

    this.focusLastSelectedDeviceItem_();
  }

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

    this.connectedDevices_ = this.systemProperties.pairedDevices.filter(
        device => device.deviceProperties.connectionState ===
            DeviceConnectionState.kConnected);
    this.unconnectedDevices_ = this.systemProperties.pairedDevices.filter(
        device => device.deviceProperties.connectionState !==
            DeviceConnectionState.kConnected);
  }

  private focusLastSelectedDeviceItem_(): void {
    const focusItem = (deviceListSelector: string, index: number): void => {
      const deviceList =
          this.shadowRoot!.querySelector<HTMLElement>(deviceListSelector);
      const items = deviceList!.shadowRoot!.querySelectorAll(
          'os-settings-paired-bluetooth-list-item');
      if (index >= items.length) {
        return;
      }
      items[index].focus();
    };

    // Search |connectedDevices_| for the device.
    let index = this.connectedDevices_.findIndex(
        device => device.deviceProperties.id === this.lastSelectedDeviceId_);
    if (index >= 0) {
      focusItem(/*deviceListSelector=*/ '#connectedDeviceList', index);
      return;
    }

    // If |connectedDevices_| doesn't contain the device, search
    // |unconnectedDevices_|.
    index = this.unconnectedDevices_.findIndex(
        device => device.deviceProperties.id === this.lastSelectedDeviceId_);
    if (index < 0) {
      return;
    }
    focusItem(/*deviceListSelector=*/ '#unconnectedDeviceList', index);
  }

  /**
   * 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;
    }

    this.announceBluetoothStateChange_();
  }

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

  private getOnOffString_(
      isBluetoothToggleOn: boolean, onString: string,
      offString: string): string {
    return isBluetoothToggleOn ? onString : offString;
  }

  private shouldShowDeviceList_(devices: PairedBluetoothDeviceProperties[]):
      boolean {
    return devices.length > 0;
  }

  private shouldShowNoDevicesFound_(): boolean {
    return !this.connectedDevices_.length && !this.unconnectedDevices_.length;
  }

  private announceBluetoothStateChange_(): void {
    getAnnouncerInstance().announce(
        this.isBluetoothToggleOn_ ? this.i18n('bluetoothEnabledA11YLabel') :
                                    this.i18n('bluetoothDisabledA11YLabel'));
  }

  private isFastPairToggleVisible_(): boolean {
    return this.isFastPairSupportedByDevice_ &&
        loadTimeData.getBoolean('enableFastPairFlag');
  }

  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;
    }

    const enabled = event.detail;
    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);
    }
  }

  /**
   * Determines if we allow access to the Saved Devices page. Unlike the Fast
   * Pair toggle, the device does not need to support Fast Pair because a device
   * could be saved to the user's account from a different device but managed on
   * this device. However Fast Pair must be enabled to confirm we have all Fast
   * Pair (and Saved Device) related code working on the device.
   */
  private isFastPairSavedDevicesRowVisible_(): boolean {
    return loadTimeData.getBoolean('enableFastPairFlag') &&
        loadTimeData.getBoolean('enableSavedDevicesFlag') &&
        !loadTimeData.getBoolean('isGuest') &&
        loadTimeData.getBoolean('isCrossDeviceFeatureSuiteEnabled');
  }

  private onClicked_(event: Event): void {
    Router.getInstance().navigateTo(routes.BLUETOOTH_SAVED_DEVICES);
    event.stopPropagation();
  }
}

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

customElements.define(
    SettingsBluetoothDevicesSubpageElement.is,
    SettingsBluetoothDevicesSubpageElement);