chromium/chrome/browser/resources/ash/settings/internet_page/network_summary_item.ts

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

/**
 * @fileoverview Polymer element for displaying the network state for a specific
 * type and a list of networks for that type. NOTE: It both Cellular and Tether
 * technologies are available, they are combined into a single 'Mobile data'
 * section. See crbug.com/726380.
 */

import 'chrome://resources/ash/common/network/network_icon.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';

import {getSimSlotCount} from 'chrome://resources/ash/common/network/cellular_utils.js';
import {CrPolicyNetworkBehaviorMojo, CrPolicyNetworkBehaviorMojoInterface} from 'chrome://resources/ash/common/network/cr_policy_network_behavior_mojo.js';
import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
import {CrToggleElement} from 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {CrPolicyIndicatorType} from 'chrome://resources/ash/common/cr_elements/policy/cr_policy_indicator_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {GlobalPolicy, VpnType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {ConnectionStateType, DeviceStateType, NetworkType, OncSource, PortalState} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {Constructor} from '../common/types.js';

import {InternetPageBrowserProxy, InternetPageBrowserProxyImpl} from './internet_page_browser_proxy.js';
import {getTemplate} from './network_summary_item.html.js';

const NetworkSummaryItemElementBase =
    mixinBehaviors([CrPolicyNetworkBehaviorMojo], I18nMixin(PolymerElement)) as
    Constructor<PolymerElement&I18nMixinInterface&
                CrPolicyNetworkBehaviorMojoInterface>;

export class NetworkSummaryItemElement extends NetworkSummaryItemElementBase {
  static get is() {
    return 'network-summary-item' as const;
  }

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

  static get properties() {
    return {
      /**
       * Device state for the network type. This might briefly be undefined if
       * a device becomes unavailable.
       */
      deviceState: {
        type: Object,
        notify: true,
      },

      /**
       * If both Cellular and Tether technologies exist, we combine the
       * sections and set this to the device state for Tether.
       */
      tetherDeviceState: Object,

      /**
       * Network state for the active network.
       */
      activeNetworkState: Object,

      /**
       * List of all network state data for the network type.
       */
      networkStateList: {
        type: Array,
        value() {
          return [];
        },
      },

      /**
       * Title line describing the network type to appear in the row's top
       * line. If it is undefined, the title text is set to a default value.
       */
      networkTitleText: String,

      /**
       * Whether to show technology badge on mobile network icon.
       */
      showTechnologyBadge_: {
        type: Boolean,
        value() {
          return loadTimeData.valueExists('showTechnologyBadge') &&
              loadTimeData.getBoolean('showTechnologyBadge');
        },
      },

      globalPolicy: Object,
    };
  }

  activeNetworkState: OncMojo.NetworkStateProperties|undefined;
  deviceState: OncMojo.DeviceStateProperties|undefined;
  globalPolicy: GlobalPolicy|undefined;
  networkStateList: OncMojo.NetworkStateProperties[];
  networkTitleText: string|undefined;
  tetherDeviceState: OncMojo.DeviceStateProperties|undefined;
  private browserProxy_: InternetPageBrowserProxy;
  private showTechnologyBadge_: boolean;

  constructor() {
    super();

    this.browserProxy_ = InternetPageBrowserProxyImpl.getInstance();
  }

  getDeviceEnabledToggle(): CrToggleElement|null {
    return this.shadowRoot!.querySelector<CrToggleElement>(
        '#deviceEnabledButton');
  }

  private getNetworkStateText_(): string {
    if (OncMojo.deviceIsInhibited(this.deviceState)) {
      return this.i18n('internetDeviceBusy');
    }

    if (OncMojo.deviceIsFlashing(this.deviceState)) {
      return this.i18n('internetDeviceFlashing');
    }

    if (this.isPortalState_(this.activeNetworkState!.portalState)) {
      if (this.deviceState && this.deviceState.type === NetworkType.kCellular) {
        return this.i18n('networkListItemCellularSignIn');
      }
      return this.i18n('networkListItemSignIn');
    }

    const stateText = this.getConnectionStateText_(this.activeNetworkState);
    if (stateText) {
      return stateText;
    }
    // No network state, use device state.
    const deviceState = this.deviceState;
    if (deviceState) {
      if (deviceState.type === NetworkType.kTether) {
        if (deviceState.deviceState === DeviceStateType.kUninitialized) {
          return this.i18n('tetherEnableBluetooth');
        }
      }

      // Enabled or enabling states.
      if (deviceState.deviceState === DeviceStateType.kEnabled) {
        return this.networkStateList.length > 0 ?
            this.i18n('networkListItemNotConnected') :
            this.i18n('networkListItemNoNetwork');
      }

      if (deviceState.deviceState === DeviceStateType.kEnabling) {
        return this.i18n('networkDeviceTurningOn');
      }
    }
    // No device or unknown device state, use 'off'.
    return this.i18n('deviceOff');
  }

  private getConnectionStateText_(networkState: OncMojo.NetworkStateProperties|
                                  undefined): string {
    if (!networkState || !networkState.guid) {
      return '';
    }
    const connectionState = networkState.connectionState;
    const name = OncMojo.getNetworkStateDisplayNameUnsafe(networkState);
    if (OncMojo.connectionStateIsConnected(connectionState)) {
      // Ethernet networks always have the display name 'Ethernet' so we use the
      // state text 'Connected' to avoid repeating the label in the sublabel.
      // See http://crbug.com/989907 for details.
      return networkState.type === NetworkType.kEthernet ?
          this.i18n('networkListItemConnected') :
          name;
    }
    if (connectionState === ConnectionStateType.kConnecting) {
      return name ?
          loadTimeData.getStringF('networkListItemConnectingTo', name) :
          this.i18n('networkListItemConnecting');
    }
    return this.i18n('networkListItemNotConnected');
  }

  private showPolicyIndicator_(activeNetworkState:
                                   OncMojo.NetworkStateProperties): boolean {
    return (activeNetworkState !== undefined &&
            OncMojo.connectionStateIsConnected(
                activeNetworkState.connectionState)) ||
        this.isPolicySource(activeNetworkState.source) ||
        this.isProhibitedVpn_();
  }

  /**
   * @return Device policy indicator for VPN when
   *     disabled by policy and an indicator corresponding to the source of the
   *     active network state otherwise.
   */
  private getPolicyIndicatorType_(activeNetworkState:
                                      OncMojo.NetworkStateProperties):
      CrPolicyIndicatorType {
    if (this.isProhibitedVpn_()) {
      return this.getIndicatorTypeForSource(OncSource.kDevicePolicy);
    }
    return this.getIndicatorTypeForSource(activeNetworkState.source);
  }

  private getNetworkStateClass_(activeNetworkState:
                                    OncMojo.NetworkStateProperties|
                                undefined): string {
    if ((this.isPortalState_(activeNetworkState!.portalState))) {
      return 'warning-message';
    }
    return 'network-state';
  }

  /**
   * @return True if the device is enabled or if it is a VPN or if
   *     we are in the state of inhibited. Note:
   *     This function will always return true for VPNs because VPNs can be
   *     disabled by policy only for built-in VPNs (OpenVPN & L2TP), but always
   *     enabled for other VPN providers. To know whether built-in VPNs are
   *     disabled, use builtInVpnProhibited_() instead.
   */
  private deviceIsEnabled_(deviceState: OncMojo.DeviceStateProperties|
                           undefined): boolean {
    if (!deviceState) {
      return false;
    }

    if (this.isInstantHotspotRebrandEnabled_() &&
        deviceState.type === NetworkType.kTether) {
      return true;
    }
    if (deviceState.type === NetworkType.kVPN) {
      return true;
    }
    if (deviceState.deviceState === DeviceStateType.kEnabled) {
      return true;
    }
    if (OncMojo.deviceIsFlashing(deviceState)) {
      return false;
    }

    return OncMojo.deviceIsInhibited(deviceState);
  }

  /**
   * @return True if the device state is enabling.
   */
  private deviceIsEnabling_(deviceState: OncMojo.DeviceStateProperties|
                            undefined): boolean {
    return !!deviceState &&
        deviceState.deviceState === DeviceStateType.kEnabling;
  }

  private deviceIsEnabledOrEnabling_(deviceState: OncMojo.DeviceStateProperties|
                                     undefined): boolean {
    return this.deviceIsEnabled_(deviceState) ||
        this.deviceIsEnabling_(deviceState);
  }

  private enableToggleIsVisible_(deviceState: OncMojo.DeviceStateProperties|
                                 undefined): boolean {
    if (!deviceState) {
      return false;
    }
    switch (deviceState.type) {
      case NetworkType.kEthernet:
      case NetworkType.kVPN:
        return false;
      case NetworkType.kTether:
        return !this.isInstantHotspotRebrandEnabled_();
      case NetworkType.kWiFi:
      case NetworkType.kCellular:
        return deviceState.deviceState !== DeviceStateType.kUninitialized;
    }
    assertNotReached();
  }

  private enableToggleIsEnabled_(deviceState: OncMojo.DeviceStateProperties|
                                 undefined): boolean {
    return this.enableToggleIsVisible_(deviceState) &&
        deviceState!.deviceState !== DeviceStateType.kProhibited &&
        !OncMojo.deviceIsInhibited(deviceState) &&
        !OncMojo.deviceStateIsIntermediate(deviceState!.deviceState) &&
        !OncMojo.deviceIsFlashing(deviceState);
  }

  private getToggleA11yString_(deviceState: OncMojo.DeviceStateProperties|
                               undefined): string {
    if (!this.enableToggleIsVisible_(deviceState)) {
      return '';
    }
    switch (deviceState!.type) {
      case NetworkType.kTether:
        return this.i18n('internetToggleTetherA11yLabel');
      case NetworkType.kCellular:
        return this.i18n('internetToggleMobileA11yLabel');
      case NetworkType.kWiFi:
        return this.i18n('internetToggleWiFiA11yLabel');
    }
    assertNotReached();
  }

  private getToggleA11yDescribedBy_(deviceState: OncMojo.DeviceStateProperties|
                                    undefined): string {
    // Use network state text to describe toggle for uninitialized tether
    // device. This announces details about enabling bluetooth.
    if (this.enableToggleIsVisible_(deviceState) &&
        deviceState!.type === NetworkType.kTether &&
        deviceState!.deviceState === DeviceStateType.kUninitialized) {
      return 'networkState';
    }
    return '';
  }

  /**
   * @return True if instant hotspot rebrand feature flag is enabled.
   */
  private isInstantHotspotRebrandEnabled_(): boolean {
    return loadTimeData.valueExists('isInstantHotspotRebrandEnabled') &&
        loadTimeData.getBoolean('isInstantHotspotRebrandEnabled');
  }

  /**
   * @return True if VPNs are disabled by policy and the current device is VPN.
   */
  private isProhibitedVpn_(): boolean {
    return !!this.deviceState && this.deviceState.type === NetworkType.kVPN &&
        this.builtInVpnProhibited_(this.deviceState);
  }

  private isBuiltInVpnType_(vpnType: VpnType): boolean {
    return vpnType === VpnType.kL2TPIPsec || vpnType === VpnType.kOpenVPN;
  }

  /**
   * @return True if at least one non-native VPN is configured.
   */
  private hasNonBuiltInVpn_(networkStateList: OncMojo.NetworkStateProperties[]):
      boolean {
    const nonBuiltInVpnIndex = networkStateList.findIndex((networkState) => {
      return !this.isBuiltInVpnType_(networkState.typeState.vpn!.type);
    });
    return nonBuiltInVpnIndex !== -1;
  }

  /**
   * @return True if the built-in VPNs are disabled by policy.
   */
  private builtInVpnProhibited_(deviceState: OncMojo.DeviceStateProperties|
                                undefined): boolean {
    return !!deviceState &&
        deviceState.deviceState === DeviceStateType.kProhibited;
  }

  /**
   * @return True if there is any configured VPN for a non-disabled
   *     VPN provider. Note: Only built-in VPN providers can be disabled by
   *     policy at the moment.
   */
  private anyVpnExists_(
      deviceState: OncMojo.DeviceStateProperties|undefined,
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    return this.hasNonBuiltInVpn_(networkStateList) ||
        (!this.builtInVpnProhibited_(deviceState) &&
         networkStateList.length > 0);
  }

  private shouldShowDetails_(
      activeNetworkState: OncMojo.NetworkStateProperties|undefined,
      deviceState: OncMojo.DeviceStateProperties|undefined,
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    if (!!deviceState && deviceState.type === NetworkType.kVPN) {
      return this.anyVpnExists_(deviceState, networkStateList);
    }

    return this.deviceIsEnabled_(deviceState) &&
        (!!activeNetworkState!.guid || networkStateList.length > 0);
  }

  private shouldShowSubpage_(
      deviceState: OncMojo.DeviceStateProperties|undefined,
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    if (!deviceState) {
      return false;
    }
    const type = deviceState.type;

    if (type === NetworkType.kTether ||
        (type === NetworkType.kCellular && this.tetherDeviceState)) {
      // The "Mobile data" subpage should always be shown if Tether is
      // available, even if there are currently no associated networks.
      return true;
    }

    if (type === NetworkType.kCellular) {
      if (OncMojo.deviceIsInhibited(deviceState)) {
        // The "Mobile data" subpage should be shown if the device state is
        // inhibited.
        return true;
      }
      // When network type is Cellular, always show "Mobile data" subpage, when
      // at least one eSIM or pSIM slot is available
      const {pSimSlots, eSimSlots} = getSimSlotCount(deviceState);
      if (eSimSlots > 0 || pSimSlots > 0) {
        return true;
      }
    }

    if (type === NetworkType.kVPN) {
      return this.anyVpnExists_(deviceState, networkStateList);
    }

    let minlen: number;
    if (type === NetworkType.kWiFi) {
      // WiFi subpage includes 'Known Networks' so always show, even if the
      // technology is still enabling / scanning, or none are visible.
      minlen = 0;
    } else {
      // By default, only show the subpage if there are 2+ networks
      minlen = 2;
    }
    return networkStateList.length >= minlen;
  }

  /**
   * This handles clicking the network summary item row. Clicking this row can
   * lead to toggling device enablement or showing the corresponding networks
   * list or showing details about a network or doing nothing based on the
   * device and networks states.
   */
  private onShowDetailsClick_(event: Event): void {
    if (!this.deviceIsEnabled_(this.deviceState)) {
      if (this.enableToggleIsEnabled_(this.deviceState)) {
        const type = this.deviceState!.type;
        const deviceEnabledToggledEvent =
            new CustomEvent('device-enabled-toggled', {
              bubbles: true,
              composed: true,
              detail: {enabled: true, type: type},
            });
        this.dispatchEvent(deviceEnabledToggledEvent);
      }
    } else if (this.isPortalState_(this.activeNetworkState!.portalState)) {
      this.browserProxy_.showPortalSignin(this.activeNetworkState!.guid);
    } else if (this.shouldShowSubpage_(
                   this.deviceState, this.networkStateList)) {
      const showNetworksEvent = new CustomEvent('show-networks', {
        bubbles: true,
        composed: true,
        detail: this.deviceState!.type,
      });
      this.dispatchEvent(showNetworksEvent);
    } else if (this.shouldShowDetails_(
                   this.activeNetworkState, this.deviceState,
                   this.networkStateList)) {
      if (this.activeNetworkState!.guid) {
        const showDetailEvent = new CustomEvent('show-detail', {
          bubbles: true,
          composed: true,
          detail: this.activeNetworkState,
        });
        this.dispatchEvent(showDetailEvent);
      } else if (this.networkStateList.length > 0) {
        const showDetailEvent = new CustomEvent('show-detail', {
          bubbles: true,
          composed: true,
          detail: this.networkStateList[0],
        });
        this.dispatchEvent(showDetailEvent);
      }
    }
    event.stopPropagation();
  }

  /**
   * This handles clicking the subpage arrow. Clicking this icon can lead
   * to showing the corresponding networks list or showing details about
   * a network or doing nothing based on the device and networks states.
   * TODO(b/253326370) Cleanup duplicate functionality between this
   * function and `onShowDetailsClick_`.
   */
  private onShowDetailsArrowClick_(event: Event): void {
    if (this.shouldShowSubpage_(this.deviceState, this.networkStateList)) {
      const showNetworksEvent = new CustomEvent('show-networks', {
        bubbles: true,
        composed: true,
        detail: this.deviceState!.type,
      });
      this.dispatchEvent(showNetworksEvent);
    } else if (this.shouldShowDetails_(
                   this.activeNetworkState, this.deviceState,
                   this.networkStateList)) {
      if (this.activeNetworkState!.guid) {
        const showDetailEvent = new CustomEvent('show-detail', {
          bubbles: true,
          composed: true,
          detail: this.activeNetworkState,
        });
        this.dispatchEvent(showDetailEvent);
      } else if (this.networkStateList.length > 0) {
        const showDetailEvent = new CustomEvent('show-detail', {
          bubbles: true,
          composed: true,
          detail: this.networkStateList[0],
        });
        this.dispatchEvent(showDetailEvent);
      }
    }
    event.stopPropagation();
  }

  private isItemActionable_(
      activeNetworkState: OncMojo.NetworkStateProperties,
      deviceState: OncMojo.DeviceStateProperties|undefined,
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    // The boolean logic here matches onShowDetailsClick_ method that handles the
    // item click event.

    if (!this.deviceIsEnabled_(this.deviceState)) {
      // When device is disabled, tapping the item flips the enable toggle. So
      // the item is actionable only when the toggle is enabled.
      return this.enableToggleIsEnabled_(this.deviceState);
    }

    // Item is actionable if tapping should show the user to the portal signin.
    if (this.isPortalState_(this.activeNetworkState!.portalState)) {
      return true;
    }

    // Item is actionable if tapping should show either networks subpage or the
    // network details page.
    return this.shouldShowSubpage_(this.deviceState, this.networkStateList) ||
        this.shouldShowDetails_(
            activeNetworkState, deviceState, networkStateList);
  }

  private showArrowButton_(
      activeNetworkState: OncMojo.NetworkStateProperties,
      deviceState: OncMojo.DeviceStateProperties|undefined,
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    if (!this.deviceIsEnabled_(deviceState)) {
      return false;
    }
    return this.shouldShowSubpage_(deviceState, networkStateList) ||
        this.shouldShowDetails_(
            activeNetworkState, deviceState, networkStateList);
  }

  /**
   * Event triggered when the enable button is toggled.
   */
  private onDeviceEnabledChange_(): void {
    assert(this.deviceState);
    const deviceIsEnabled = this.deviceIsEnabled_(this.deviceState);
    const deviceEnabledToggledEvent =
        new CustomEvent('device-enabled-toggled', {
          bubbles: true,
          composed: true,
          detail: {enabled: !deviceIsEnabled, type: this.deviceState.type},
        });
    this.dispatchEvent(deviceEnabledToggledEvent);

    // Set the device state to enabling or disabling until updated.
    this.deviceState.deviceState = deviceIsEnabled ?
        DeviceStateType.kDisabling :
        DeviceStateType.kEnabling;
  }

  private getTitleText_(): string {
    if (this.networkTitleText) {
      return this.networkTitleText;
    }
    if (this.isPortalState_(this.activeNetworkState!.portalState)) {
      const stateText = this.getConnectionStateText_(this.activeNetworkState);
      if (stateText) {
        return stateText;
      }
    }
    return this.getNetworkTypeString_(this.activeNetworkState!.type);
  }

  /**
   * Make sure events in embedded components do not propagate to onDetailsClick_.
   */
  private doNothing_(event: Event): void {
    event.stopPropagation();
  }

  private getNetworkTypeString_(type: NetworkType): string {
    // The shared Cellular/Tether subpage is referred to as "Mobile".
    // TODO(khorimoto): Remove once Cellular/Tether are split into their own
    // sections.
    if (type === NetworkType.kCellular ||
        (type === NetworkType.kTether &&
         !this.isInstantHotspotRebrandEnabled_())) {
      type = NetworkType.kMobile;
    }
    return this.i18n('OncType' + OncMojo.getNetworkTypeString(type));
  }

  private isPortalState_(portalState: PortalState): boolean {
    return portalState === PortalState.kPortal ||
        portalState === PortalState.kPortalSuspected;
  }
}

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

customElements.define(NetworkSummaryItemElement.is, NetworkSummaryItemElement);