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

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

/**
 * @fileoverview
 * 'os-settings-menu' shows a menu with a hardcoded set of pages and subpages.
 */
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import '../settings_shared.css.js';
import '../os_settings_icons.html.js';
import './menu_item.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 {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin, WebUiListenerMixinInterface} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js';
import {NetworkListenerBehavior, NetworkListenerBehaviorInterface} from 'chrome://resources/ash/common/network/network_listener_behavior.js';
import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, PairedBluetoothDeviceProperties, SystemPropertiesObserverReceiver as BluetoothPropertiesObserverReceiver} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {CrosNetworkConfigInterface, FilterType, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {IronSelectorElement} from 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import {DomRepeat, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {assertExists, castExists} from '../assert_extras.js';
import {androidAppsVisible, isInputDeviceSettingsSplitEnabled, isRevampWayfindingEnabled} from '../common/load_time_booleans.js';
import {RouteObserverMixin, RouteObserverMixinInterface} from '../common/route_observer_mixin.js';
import {Constructor} from '../common/types.js';
import {DevicePageBrowserProxy, DevicePageBrowserProxyImpl} from '../device_page/device_page_browser_proxy.js';
import {FakeInputDeviceSettingsProvider} from '../device_page/fake_input_device_settings_provider.js';
import {getInputDeviceSettingsProvider} from '../device_page/input_device_mojo_interface_provider.js';
import {InputDeviceSettingsProviderInterface, Keyboard, Mouse, PointingStick, Touchpad} from '../device_page/input_device_settings_types.js';
import {KeyboardSettingsObserverReceiver, MouseSettingsObserverReceiver, PointingStickSettingsObserverReceiver, TouchpadSettingsObserverReceiver} from '../mojom-webui/input_device_settings_provider.mojom-webui.js';
import * as routesMojom from '../mojom-webui/routes.mojom-webui.js';
import {MultiDeviceBrowserProxy, MultiDeviceBrowserProxyImpl} from '../multidevice_page/multidevice_browser_proxy.js';
import {MultiDevicePageContentData, MultiDeviceSettingsMode} from '../multidevice_page/multidevice_constants.js';
import {OsPageAvailability} from '../os_page_availability.js';
import {AccountManagerBrowserProxyImpl} from '../os_people_page/account_manager_browser_proxy.js';
import {isAdvancedRoute, Route, Router} from '../router.js';

import {getTemplate} from './os_settings_menu.html.js';

const {Section} = routesMojom;

interface MenuItemData {
  section: routesMojom.Section;
  path: string;
  icon: string;
  label: string;

  // Sublabels should only exist when OsSettingsRevampWayfinding is enabled.
  sublabel?: string;
}

export interface OsSettingsMenuElement {
  $: {
    topMenu: IronSelectorElement,
    topMenuRepeat: DomRepeat,
  };
}

/**
 * Returns a copy of the given `str` with the first letter capitalized according
 * to the locale.
 */
function capitalize(str: string): string {
  const firstChar = str.charAt(0).toLocaleUpperCase();
  const remainingStr = str.slice(1);
  return `${firstChar}${remainingStr}`;
}

function getPrioritizedConnectedNetwork(
    networkStateList: OncMojo.NetworkStateProperties[]):
    OncMojo.NetworkStateProperties|null {
  // The priority of the network types. Both Cellular and Tether belongs to
  // the Mobile Data.
  const orderedNetworkTypes = [
    NetworkType.kEthernet,
    NetworkType.kWiFi,
    NetworkType.kCellular,
    NetworkType.kTether,
    NetworkType.kVPN,
  ];

  const networkStates:
      Record<NetworkType, OncMojo.NetworkStateProperties[]> = {};

  for (const networkType of orderedNetworkTypes) {
    networkStates[networkType] = [];
  }

  for (const networkState of networkStateList) {
    networkStates[networkState.type].push(networkState);
  }

  for (const type of orderedNetworkTypes) {
    for (const networkState of networkStates[type]) {
      if (OncMojo.connectionStateIsConnected(networkState.connectionState)) {
        return networkState;
      }
    }
  }

  return null;
}

const OsSettingsMenuElementBase =
    mixinBehaviors(
        [NetworkListenerBehavior],
        WebUiListenerMixin(RouteObserverMixin(I18nMixin(PolymerElement)))) as
    Constructor<PolymerElement&I18nMixinInterface&WebUiListenerMixinInterface&
                RouteObserverMixinInterface&NetworkListenerBehaviorInterface>;

export class OsSettingsMenuElement extends OsSettingsMenuElementBase {
  static get is() {
    return 'os-settings-menu' as const;
  }

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

  static get properties() {
    return {
      /**
       * Determines which menu items are available for their respective pages
       */
      pageAvailability: {
        type: Object,
      },

      advancedOpened: {
        type: Boolean,
        value: false,
        notify: true,
      },

      /**
       * If this menu exists in the drawer. Used to compute responsiveness in
       * smaller window sizes.
       */
      isDrawerMenu: {
        type: Boolean,
        value: false,
      },

      basicMenuItems_: {
        type: Array,
        computed: 'computeBasicMenuItems_(pageAvailability.*,' +
            'accountsMenuItemDescription_,' +
            'bluetoothMenuItemDescription_,' +
            'deviceMenuItemDescription_,' +
            'internetMenuItemDescription_,' +
            'multideviceMenuItemDescription_)',
        readOnly: true,
      },

      advancedMenuItems_: {
        type: Array,
        computed: 'computeAdvancedMenuItems_(pageAvailability.*)',
        readOnly: true,
      },

      /**
       * The path of the currently selected menu item. e.g. '/internet'.
       */
      selectedItemPath_: {
        type: String,
        value: '',
      },

      aboutMenuItemPath_: {
        type: String,
        value: `/${routesMojom.ABOUT_CHROME_OS_SECTION_PATH}`,
      },

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

      accountsMenuItemDescription_: {
        type: String,
        value(this: OsSettingsMenuElement) {
          return this.i18n('primaryUserEmail');
        },
      },

      bluetoothMenuItemDescription_: {
        type: String,
        value: '',
      },

      hasKeyboard_: Boolean,

      hasMouse_: Boolean,

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

      hasTouchpad_: Boolean,

      deviceMenuItemDescription_: {
        type: String,
        value: '',
        computed: 'computeDeviceMenuItemDescription_(hasKeyboard_,' +
            'hasMouse_, hasPointingStick_, hasTouchpad_)',
      },

      multideviceMenuItemDescription_: {
        type: String,
        value: '',
      },

      internetMenuItemDescription_: {
        type: String,
        value: '',
      },

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

  advancedOpened: boolean;
  isDrawerMenu: boolean;
  pageAvailability: OsPageAvailability;
  private basicMenuItems_: MenuItemData[];
  private advancedMenuItems_: MenuItemData[];
  private isRevampWayfindingEnabled_: boolean;
  private isRtl_: boolean;
  private selectedItemPath_: string;
  private aboutMenuItemPath_: string;

  // Accounts section members.
  private accountsMenuItemDescription_: string;

  // Bluetooth section members.
  private bluetoothMenuItemDescription_: string;
  private bluetoothPropertiesObserverReceiver_:
      BluetoothPropertiesObserverReceiver|undefined;

  // Device section members.
  private deviceMenuItemDescription_: string;
  private hasKeyboard_: boolean|undefined;
  private hasMouse_: boolean|undefined;
  private hasPointingStick_: boolean|undefined;
  private hasTouchpad_: boolean|undefined;
  private isInputDeviceSettingsSplitEnabled_: boolean =
      isInputDeviceSettingsSplitEnabled();
  private devicePageBrowserProxy_: DevicePageBrowserProxy;
  private inputDeviceSettingsProvider_: InputDeviceSettingsProviderInterface;
  private keyboardSettingsObserverReceiver_: KeyboardSettingsObserverReceiver|
      undefined;
  private mouseSettingsObserverReceiver_: MouseSettingsObserverReceiver|
      undefined;
  private pointingStickSettingsObserverReceiver_:
      PointingStickSettingsObserverReceiver|undefined;
  private touchpadSettingsObserverReceiver_: TouchpadSettingsObserverReceiver|
      undefined;

  // Internet section members.
  private networkConfig_: CrosNetworkConfigInterface;
  private internetMenuItemDescription_: string;
  private isDeviceCellularCapable_: boolean;

  // Multidevice section members.
  private multideviceBrowserProxy_: MultiDeviceBrowserProxy;
  private multideviceMenuItemDescription_: string;

  constructor() {
    super();

    if (this.isInputDeviceSettingsSplitEnabled_) {
      this.inputDeviceSettingsProvider_ = getInputDeviceSettingsProvider();
    } else {
      this.devicePageBrowserProxy_ = DevicePageBrowserProxyImpl.getInstance();
    }
    this.multideviceBrowserProxy_ = MultiDeviceBrowserProxyImpl.getInstance();
  }

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

    if (this.isRevampWayfindingEnabled_) {
      // Accounts menu item is not available in guest mode.
      if (this.pageAvailability[Section.kPeople]) {
        this.updateAccountsMenuItemDescription_();
        this.addWebUiListener(
            'accounts-changed',
            this.updateAccountsMenuItemDescription_.bind(this));
      }

      // Bluetooth menu item.
      this.observeBluetoothProperties_();

      // Device menu item.
      if (this.isInputDeviceSettingsSplitEnabled_) {
        this.observeKeyboardSettings_();
        this.observeMouseSettings_();
        this.observePointingStickSettings_();
        this.observeTouchpadSettings_();
      } else {
        // Before input device settings split, keyboard was always assumed to
        // exist.
        this.hasKeyboard_ = true;
        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.devicePageBrowserProxy_.initializePointers();
      }

      // Internet menu item.
      this.networkConfig_ =
          MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
      this.computeIsDeviceCellularCapable_().then(() => {
        this.updateInternetMenuItemDescription_();
      });

      // Multidevice menu item is not available in guest mode.
      if (this.pageAvailability[Section.kMultiDevice]) {
        this.addWebUiListener(
            'settings.updateMultidevicePageContentData',
            this.updateMultideviceMenuItemDescription_.bind(this));

        this.multideviceBrowserProxy_.getPageContentData().then(
            this.updateMultideviceMenuItemDescription_.bind(this));
      }
    }

    this.isRtl_ = window.getComputedStyle(this).direction === 'rtl';
  }

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

    this.bluetoothPropertiesObserverReceiver_?.$.close();

    // The following receivers are undefined in tests.
    this.keyboardSettingsObserverReceiver_?.$.close();
    this.mouseSettingsObserverReceiver_?.$.close();
    this.pointingStickSettingsObserverReceiver_?.$.close();
    this.touchpadSettingsObserverReceiver_?.$.close();
  }

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

    // Force render menu items so the matching item can be selected when the
    // page initially loads.
    this.$.topMenuRepeat.render();
  }

  override currentRouteChanged(newRoute: Route): void {
    const urlSearchQuery =
        Router.getInstance().getQueryParameters().get('search');
    // If the route navigated to by a search result is in the advanced
    // section, the advanced menu will expand.
    if (urlSearchQuery && isAdvancedRoute(newRoute)) {
      this.advancedOpened = true;
    }

    this.setSelectedItemPathForRoute_(newRoute);
  }

  /**
   * The selected menu item should be the menu item whose path matches the path
   * of the section ancestor route for the given `route`. For example, the
   * BLUETOOTH_DEVICES_SUBPAGE route's section ancestor is the BLUETOOTH route,
   * whose path matches the bluetooth menu item path.
   */
  private setSelectedItemPathForRoute_(route: Route): void {
    const sectionAncestorRoute = route.getSectionAncestor();
    if (sectionAncestorRoute) {
      const menuItems =
          this.shadowRoot!.querySelectorAll('os-settings-menu-item');
      for (const menuItem of menuItems) {
        if (sectionAncestorRoute.path === menuItem.path) {
          this.setSelectedItemPath_(menuItem.path);
          return;
        }
      }
    }

    // Nothing is selected.
    this.setSelectedItemPath_('');
  }

  private computeBasicMenuItems_(): MenuItemData[] {
    let basicMenuItems: MenuItemData[];
    if (this.isRevampWayfindingEnabled_) {
      basicMenuItems = [
        {
          section: Section.kNetwork,
          path: `/${routesMojom.NETWORK_SECTION_PATH}`,
          icon: 'os-settings:network-wifi',
          label: this.i18n('internetPageTitle'),
          sublabel: this.internetMenuItemDescription_,
        },
        {
          section: Section.kBluetooth,
          path: `/${routesMojom.BLUETOOTH_SECTION_PATH}`,
          icon: 'cr:bluetooth',
          label: this.i18n('bluetoothPageTitle'),
          sublabel: this.bluetoothMenuItemDescription_,
        },
        {
          section: Section.kMultiDevice,
          path: `/${routesMojom.MULTI_DEVICE_SECTION_PATH}`,
          icon: 'os-settings:connected-devices',
          label: this.i18n('multidevicePageTitle'),
          sublabel: this.multideviceMenuItemDescription_,
        },
        {
          section: Section.kPeople,
          path: `/${routesMojom.PEOPLE_SECTION_PATH}`,
          icon: 'os-settings:account',
          label: this.i18n('osPeoplePageTitle'),
          sublabel: this.accountsMenuItemDescription_,
        },
        {
          section: Section.kKerberos,
          path: `/${routesMojom.KERBEROS_SECTION_PATH}`,
          icon: 'os-settings:auth-key',
          label: this.i18n('kerberosPageTitle'),
        },
        {
          section: Section.kDevice,
          path: `/${routesMojom.DEVICE_SECTION_PATH}`,
          icon: 'os-settings:laptop-chromebook',
          label: this.i18n('devicePageTitle'),
          sublabel: this.deviceMenuItemDescription_,
        },
        {
          section: Section.kPersonalization,
          path: `/${routesMojom.PERSONALIZATION_SECTION_PATH}`,
          icon: 'os-settings:personalization',
          label: this.i18n('personalizationPageTitle'),
          sublabel: this.i18n('personalizationMenuItemDescription'),
        },
        {
          section: Section.kPrivacyAndSecurity,
          path: `/${routesMojom.PRIVACY_AND_SECURITY_SECTION_PATH}`,
          icon: 'cr:security',
          label: this.i18n('privacyPageTitle'),
          sublabel: this.i18n('privacyMenuItemDescription'),
        },
        {
          section: Section.kApps,
          path: `/${routesMojom.APPS_SECTION_PATH}`,
          icon: 'os-settings:apps',
          label: this.i18n('appsPageTitle'),
          sublabel: androidAppsVisible() ?
              this.i18n('appsMenuItemDescription') :
              this.i18n('appsmenuItemDescriptionArcUnavailable'),
        },
        {
          section: Section.kAccessibility,
          path: `/${routesMojom.ACCESSIBILITY_SECTION_PATH}`,
          icon: 'os-settings:accessibility-revamp',
          label: this.i18n('a11yPageTitle'),
          sublabel: this.i18n('a11yMenuItemDescription'),
        },
        {
          section: Section.kSystemPreferences,
          path: `/${routesMojom.SYSTEM_PREFERENCES_SECTION_PATH}`,
          icon: 'os-settings:system-preferences',
          label: this.i18n('systemPreferencesTitle'),
          sublabel: this.i18n('systemPreferencesMenuItemDescription'),
        },
        {
          section: Section.kAboutChromeOs,
          path: this.aboutMenuItemPath_,
          icon: 'os-settings:chrome',
          label: this.i18n('aboutOsPageTitle'),
          sublabel: this.i18n('aboutChromeOsMenuItemDescription'),
        },
      ];
    } else {
      basicMenuItems = [
        {
          section: Section.kNetwork,
          path: `/${routesMojom.NETWORK_SECTION_PATH}`,
          icon: 'os-settings:network-wifi',
          label: this.i18n('internetPageTitle'),
        },
        {
          section: Section.kBluetooth,
          path: `/${routesMojom.BLUETOOTH_SECTION_PATH}`,
          icon: 'cr:bluetooth',
          label: this.i18n('bluetoothPageTitle'),
        },
        {
          section: Section.kMultiDevice,
          path: `/${routesMojom.MULTI_DEVICE_SECTION_PATH}`,
          icon: 'os-settings:multidevice-better-together-suite',
          label: this.i18n('multidevicePageTitle'),
        },
        {
          section: Section.kPeople,
          path: `/${routesMojom.PEOPLE_SECTION_PATH}`,
          icon: 'cr:person',
          label: this.i18n('osPeoplePageTitle'),
        },
        {
          section: Section.kKerberos,
          path: `/${routesMojom.KERBEROS_SECTION_PATH}`,
          icon: 'os-settings:auth-key',
          label: this.i18n('kerberosPageTitle'),
        },
        {
          section: Section.kDevice,
          path: `/${routesMojom.DEVICE_SECTION_PATH}`,
          icon: 'os-settings:laptop-chromebook',
          label: this.i18n('devicePageTitle'),
        },
        {
          section: Section.kPersonalization,
          path: `/${routesMojom.PERSONALIZATION_SECTION_PATH}`,
          icon: 'os-settings:paint-brush',
          label: this.i18n('personalizationPageTitle'),
        },
        {
          section: Section.kSearchAndAssistant,
          path: `/${routesMojom.SEARCH_AND_ASSISTANT_SECTION_PATH}`,
          icon: 'cr:search',
          label: this.i18n('osSearchPageTitle'),
        },
        {
          section: Section.kPrivacyAndSecurity,
          path: `/${routesMojom.PRIVACY_AND_SECURITY_SECTION_PATH}`,
          icon: 'cr:security',
          label: this.i18n('privacyPageTitle'),
        },
        {
          section: Section.kApps,
          path: `/${routesMojom.APPS_SECTION_PATH}`,
          icon: 'os-settings:apps',
          label: this.i18n('appsPageTitle'),
        },
        {
          section: Section.kAccessibility,
          path: `/${routesMojom.ACCESSIBILITY_SECTION_PATH}`,
          icon: 'os-settings:accessibility',
          label: this.i18n('a11yPageTitle'),
        },
      ];
    }

    return basicMenuItems.filter(
        ({section}) => !!this.pageAvailability[section]);
  }

  private computeAdvancedMenuItems_(): MenuItemData[] {
    // When OsSettingsRevampWayfinding is enabled, there is no Advanced menu.
    if (this.isRevampWayfindingEnabled_) {
      return [];
    }

    const advancedMenuItems: MenuItemData[] = [
      {
        section: Section.kDateAndTime,
        path: `/${routesMojom.DATE_AND_TIME_SECTION_PATH}`,
        icon: 'os-settings:clock',
        label: this.i18n('dateTimePageTitle'),
      },
      {
        section: Section.kLanguagesAndInput,
        path: `/${routesMojom.LANGUAGES_AND_INPUT_SECTION_PATH}`,
        icon: 'os-settings:language',
        label: this.i18n('osLanguagesPageTitle'),
      },
      {
        section: Section.kFiles,
        path: `/${routesMojom.FILES_SECTION_PATH}`,
        icon: 'os-settings:folder-outline',
        label: this.i18n('filesPageTitle'),
      },
      {
        section: Section.kPrinting,
        path: `/${routesMojom.PRINTING_SECTION_PATH}`,
        icon: 'os-settings:print',
        label: this.i18n('printingPageTitle'),
      },
      {
        section: Section.kCrostini,
        path: `/${routesMojom.CROSTINI_SECTION_PATH}`,
        icon: 'os-settings:developer-tags',
        label: this.i18n('crostiniPageTitle'),
      },
      {
        section: Section.kReset,
        path: `/${routesMojom.RESET_SECTION_PATH}`,
        icon: 'os-settings:restore',
        label: this.i18n('resetPageTitle'),
      },
    ];

    return advancedMenuItems.filter(
        ({section}) => !!this.pageAvailability[section]);
  }

  private onAdvancedButtonToggle_(): void {
    this.advancedOpened = !this.advancedOpened;
  }

  /**
   * @param path The path of the menu item to be selected. This path should be
   * the pathname portion of a URL, not the full URL. e.g. `/internet`, not
   * `chrome://os-settings/internet`.
   */
  private setSelectedItemPath_(path: string): void {
    this.selectedItemPath_ = path;
  }

  /**
   * Called when a selectable item from <iron-selector> is clicked. This is
   * fired before the selected item is changed.
   */
  private onItemActivated_(event: CustomEvent<{selected: string}>): void {
    this.setSelectedItemPath_(event.detail.selected);
  }

  private onItemSelected_(e: CustomEvent<{item: HTMLElement}>): void {
    e.detail.item.setAttribute('aria-current', 'true');
  }

  private onItemDeselected_(e: CustomEvent<{item: HTMLElement}>): void {
    e.detail.item.removeAttribute('aria-current');
  }

  /**
   * @param opened Whether the menu is expanded.
   * @return Which icon to use.
   */
  private arrowState_(opened: boolean): string {
    return opened ? 'cr:arrow-drop-up' : 'cr:arrow-drop-down';
  }

  private boolToString_(bool: boolean): string {
    return bool.toString();
  }

  private getMenuItemTooltipPosition_(): 'right'|'left'|'bottom' {
    if (this.isDrawerMenu) {
      return 'bottom';
    }

    return this.isRtl_ ? 'left' : 'right';
  }

  /**
   * Updates the "Accounts" menu item description to one of the following:
   * - If there are multiple accounts (> 1), show "N accounts".
   * - If there is only one account, show the account email.
   */
  private async updateAccountsMenuItemDescription_(): Promise<void> {
    const accounts =
        await AccountManagerBrowserProxyImpl.getInstance().getAccounts();
    if (accounts.length > 1) {
      this.accountsMenuItemDescription_ =
          this.i18n('accountsMenuItemDescription', accounts.length);
      return;
    }
    const deviceAccount = accounts.find(account => account.isDeviceAccount);
    assertExists(deviceAccount, 'No device account found.');
    this.accountsMenuItemDescription_ = deviceAccount.email;
  }

  private observeBluetoothProperties_(): void {
    this.bluetoothPropertiesObserverReceiver_ =
        new BluetoothPropertiesObserverReceiver(this);
    getBluetoothConfig().observeSystemProperties(
        this.bluetoothPropertiesObserverReceiver_.$.bindNewPipeAndPassRemote());
  }

  /** Implements SystemPropertiesObserverInterface */
  onPropertiesUpdated(properties: BluetoothSystemProperties): void {
    const isBluetoothOn =
        properties.systemState === BluetoothSystemState.kEnabled ||
        properties.systemState === BluetoothSystemState.kEnabling;
    const connectedDevices = properties.pairedDevices.filter(
        (device) => device.deviceProperties.connectionState ===
            DeviceConnectionState.kConnected);
    this.updateBluetoothMenuItemDescription_(isBluetoothOn, connectedDevices);
  }

  /**
   * Updates the "Bluetooth" menu item description to one of the following:
   * - If bluetooth is off, show "Off".
   * - If bluetooth is on but no bluetooth devices are connected, show "On".
   * - If one device is connected, show the name of the device.
   * - If there are multiple devices connected, show "N devices connected".
   */
  private updateBluetoothMenuItemDescription_(
      isBluetoothOn: boolean,
      connectedDevices: PairedBluetoothDeviceProperties[]): void {
    if (connectedDevices.length === 0) {
      this.bluetoothMenuItemDescription_ =
          isBluetoothOn ? this.i18n('deviceOn') : this.i18n('deviceOff');
      return;
    }

    if (connectedDevices.length === 1) {
      const device = castExists(connectedDevices[0]);
      this.bluetoothMenuItemDescription_ = getDeviceNameUnsafe(device);
      return;
    }

    this.bluetoothMenuItemDescription_ = this.i18n(
        'bluetoothMenuItemDescriptionMultipleDevicesConnected',
        connectedDevices.length);
  }

  /** NetworkListenerBehavior override */
  override onNetworkStateListChanged(): void {
    this.updateInternetMenuItemDescription_();
  }

  /** NetworkListenerBehavior override */
  override onDeviceStateListChanged(): void {
    this.updateInternetMenuItemDescription_();
  }

  /** NetworkListenerBehavior override */
  override onActiveNetworksChanged(): void {
    this.updateInternetMenuItemDescription_();
  }

  private async computeIsDeviceCellularCapable_(): Promise<void> {
    const {result: deviceStateList} =
        await this.networkConfig_.getDeviceStateList();
    const cellularDeviceState = deviceStateList.find(
        deviceState => deviceState.type === NetworkType.kCellular);
    this.isDeviceCellularCapable_ = !!cellularDeviceState;
  }

  private async isInstantHotspotAvailable_(): Promise<boolean> {
    const {result: deviceStateList} =
        await this.networkConfig_.getDeviceStateList();
    const tetherDeviceState = deviceStateList.find(
        deviceState => deviceState.type === NetworkType.kTether);

    return !!tetherDeviceState;
  }

  /**
   * Updates the "Internet" menu item description to one of the followings:
   * - If there are networks connected, show the name of one connected network
   *   with the priority: Ethernet, Wi-Fi, mobile(Cellular, Tether) and VPN.
   * - If there is no networks connected but instant hotspot is available, show
   * "Instant hotspot available".
   * - If there is no networks connected and mobile data is not supported, show
   * "Wi-Fi".
   * - If there is no networks connected but mobile data is supported, show
   * "Wi-Fi, mobile data".
   */
  private async updateInternetMenuItemDescription_(): Promise<void> {
    // Return early if the feature revamp wayfinding is not enabled since
    // `networkConfig_` is not defined and we don't need to show the description
    // if the feature is disabled.
    if (!this.isRevampWayfindingEnabled_) {
      return;
    }

    const {result: networkStateList} =
        await this.networkConfig_.getNetworkStateList({
          filter: FilterType.kVisible,
          limit: NO_LIMIT,
          networkType: NetworkType.kAll,
        });

    const prioritizedConnectedNetwork =
        getPrioritizedConnectedNetwork(networkStateList);
    if (prioritizedConnectedNetwork) {
      this.internetMenuItemDescription_ = prioritizedConnectedNetwork.name;
      return;
    }

    const tetherNetworkState = networkStateList.find(
        networkState => networkState.type === NetworkType.kTether);
    if (tetherNetworkState && await this.isInstantHotspotAvailable_()) {
      this.internetMenuItemDescription_ =
          this.i18n('internetMenuItemDescriptionInstantHotspotAvailable');
      return;
    }

    if (this.isDeviceCellularCapable_) {
      this.internetMenuItemDescription_ =
          this.i18n('internetMenuItemDescriptionWifiAndMobileData');
      return;
    }

    this.internetMenuItemDescription_ =
        this.i18n('internetMenuItemDescriptionWifi');
  }

  /**
   * Updates the "Multidevice" menu item description to one of the following:
   * - If there is a phone connected, show "Connected to <phone name>".
   * - If there is a phone connected but the device name is missing, show
   *   "Connected to Android phone".
   * - If there is no phone connected, show "Phone Hub, Nearby Share".
   */
  private updateMultideviceMenuItemDescription_(
      pageContentData: MultiDevicePageContentData): void {
    if (!this.isRevampWayfindingEnabled_) {
      return;
    }

    if (pageContentData.mode === MultiDeviceSettingsMode.HOST_SET_VERIFIED) {
      if (pageContentData.hostDeviceName) {
        this.multideviceMenuItemDescription_ = this.i18n(
            'multideviceMenuItemDescriptionPhoneConnected',
            pageContentData.hostDeviceName);
      } else {
        this.multideviceMenuItemDescription_ =
            this.i18n('multideviceMenuItemDescriptionDeviceNameMissing');
      }
      return;
    }

    this.multideviceMenuItemDescription_ =
        this.i18n('multideviceMenuItemDescription');
  }

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

    this.keyboardSettingsObserverReceiver_ =
        new KeyboardSettingsObserverReceiver(this);
    this.inputDeviceSettingsProvider_.observeKeyboardSettings(
        this.keyboardSettingsObserverReceiver_.$.bindNewPipeAndPassRemote());
  }

  /** Implements KeyboardSettingsObserverInterface */
  onKeyboardListUpdated(keyboards: Keyboard[]): void {
    this.hasKeyboard_ = keyboards.length > 0;
  }

  /** Implements KeyboardSettingsObserverInterface */
  onKeyboardPoliciesUpdated(): void {
    // Not handled.
  }

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

    this.mouseSettingsObserverReceiver_ =
        new MouseSettingsObserverReceiver(this);
    this.inputDeviceSettingsProvider_.observeMouseSettings(
        this.mouseSettingsObserverReceiver_.$.bindNewPipeAndPassRemote());
  }

  /** Implements MouseSettingsObserverInterface */
  onMouseListUpdated(mice: Mouse[]): void {
    this.hasMouse_ = mice.length > 0;
  }

  /** Implements MouseSettingsObserverInterface */
  onMousePoliciesUpdated(): void {
    // Not handled.
  }

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

    this.pointingStickSettingsObserverReceiver_ =
        new PointingStickSettingsObserverReceiver(this);
    this.inputDeviceSettingsProvider_.observePointingStickSettings(
        this.pointingStickSettingsObserverReceiver_.$
            .bindNewPipeAndPassRemote());
  }

  /** Implements PointingStickSettingsObserverInterface */
  onPointingStickListUpdated(pointingSticks: PointingStick[]): void {
    this.hasPointingStick_ = pointingSticks.length > 0;
  }

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

    this.touchpadSettingsObserverReceiver_ =
        new TouchpadSettingsObserverReceiver(this);
    this.inputDeviceSettingsProvider_.observeTouchpadSettings(
        this.touchpadSettingsObserverReceiver_.$.bindNewPipeAndPassRemote());
  }

  /** Implements TouchpadSettingsObserverInterface */
  onTouchpadListUpdated(touchpads: Touchpad[]): void {
    this.hasTouchpad_ = touchpads.length > 0;
  }

  /**
   * Only show at most 3 strings in order of priority:
   * - "keyboard" (if available)
   * - "mouse" OR "touchpad" (if available, prioritize mouse)
   * - "print"
   * - "display"
   */
  private computeDeviceMenuItemDescription_(): string {
    if (!this.isRevampWayfindingEnabled_) {
      return '';
    }

    const wordOptions: string[] = [];

    if (this.hasKeyboard_) {
      wordOptions.push(this.i18n('deviceMenuItemDescriptionKeyboard'));
    }

    if (this.hasMouse_ || this.hasPointingStick_) {
      wordOptions.push(this.i18n('deviceMenuItemDescriptionMouse'));
    } else if (this.hasTouchpad_) {
      wordOptions.push(this.i18n('deviceMenuItemDescriptionTouchpad'));
    }

    wordOptions.push(
        this.i18n('deviceMenuItemDescriptionPrint'),
        this.i18n('deviceMenuItemDescriptionDisplay'));

    const words = wordOptions.slice(0, 3);
    return capitalize(words.join(this.i18n('listSeparator')));
  }
}

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

customElements.define(OsSettingsMenuElement.is, OsSettingsMenuElement);