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

// Copyright 2017 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 information about WiFi,
 * Cellular, or virtual networks.
 */

import 'chrome://resources/ash/common/network/network_list.js';
import 'chrome://resources/ash/common/cr_elements/localized_link/localized_link.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/cr_shared_style.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_toggle/cr_toggle.js';
import 'chrome://resources/ash/common/cr_elements/md_select.css.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_policy_indicator.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import '../settings_shared.css.js';
import '../os_settings_icons.css.js';
import './cellular_networks_list.js';
import './network_always_on_vpn.js';
import './internet_subpage_menu.js';
import '/shared/settings/prefs/prefs.js';

import {PrefsMixin, PrefsMixinInterface} from '/shared/settings/prefs/prefs_mixin.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {CrPolicyNetworkBehaviorMojo, CrPolicyNetworkBehaviorMojoInterface} from 'chrome://resources/ash/common/network/cr_policy_network_behavior_mojo.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 {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {AlwaysOnVpnMode, AlwaysOnVpnProperties, CrosNetworkConfigInterface, FilterType, GlobalPolicy, NO_LIMIT, VpnProvider, VpnType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {ConnectionStateType, DeviceStateType, NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {afterNextRender, DomRepeatEvent, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {castExists} from '../assert_extras.js';
import {DeepLinkingMixin, DeepLinkingMixinInterface} from '../common/deep_linking_mixin.js';
import {RouteOriginMixin, RouteOriginMixinInterface} from '../common/route_origin_mixin.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, Router, routes} from '../router.js';

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

const SettingsInternetSubpageElementBase =
    mixinBehaviors(
        [
          NetworkListenerBehavior,
          CrPolicyNetworkBehaviorMojo,
        ],
        DeepLinkingMixin(
            PrefsMixin(RouteOriginMixin(I18nMixin(PolymerElement))))) as {
      new (): PolymerElement & I18nMixinInterface & RouteOriginMixinInterface &
          PrefsMixinInterface & DeepLinkingMixinInterface &
          NetworkListenerBehaviorInterface &
          CrPolicyNetworkBehaviorMojoInterface,
    };

export class SettingsInternetSubpageElement extends
    SettingsInternetSubpageElementBase {
  static get is() {
    return 'settings-internet-subpage' as const;
  }

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

  static get properties() {
    return {
      /**
       * Highest priority connected network or null. Provided by
       * settings-internet-page (but set in network-summary).
       */
      defaultNetwork: Object,

      /**
       * Device state for the network type. Note: when both Cellular and Tether
       * are available this will always be set to the Cellular device state and
       * |tetherDeviceState| will be set to the Tether device state.
       */
      deviceState: Object,

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

      globalPolicy: Object,

      /**
       * List of third party (Extension + Arc) VPN providers.
       */
      vpnProviders: Array,

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

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

      isConnectedToNonCellularNetwork: {
        type: Boolean,
      },

      isCellularSetupActive: {
        type: Boolean,
      },

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

      /**
       * Dictionary of lists of network states for third party VPNs.
       */
      thirdPartyVpns_: {
        type: Object,
        value() {
          return {};
        },
      },

      /**
       * Return true if instant hotspot rebrand feature flag is enabled.
       */
      isInstantHotspotRebrandEnabled_: {
        type: Boolean,
        value() {
          return loadTimeData.valueExists('isInstantHotspotRebrandEnabled') &&
              loadTimeData.getBoolean('isInstantHotspotRebrandEnabled');
        },
      },

      isShowingVpn_: {
        type: Boolean,
        computed: 'computeIsShowingVpn_(deviceState)',
        reflectToAttribute: true,
      },

      isShowingTether_: {
        type: Boolean,
        computed: 'computeIsShowingTether_(deviceState)',
        reflectToAttribute: true,
      },

      /**
       * Whether the browser/ChromeOS is managed by their organization
       * through enterprise policies.
       */
      isManaged_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('isManaged');
        },
      },

      /**
       * Always-on VPN operating mode.
       */
      alwaysOnVpnMode_: Number,

      /**
       * Always-on VPN service automatically started on login.
       */
      alwaysOnVpnService_: String,

      /**
       * List of potential Tether hosts whose "Google Play Services"
       * notifications are disabled (these notifications are required to use
       * Instant Tethering).
       */
      notificationsDisabledDeviceNames_: {
        type: Array,
        value() {
          return [];
        },
      },

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

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

      /**
       * False if VPN is disabled by policy.
       */
      vpnIsEnabled_: {
        type: Boolean,
        value: false,
      },

      /**
       * Contains the settingId of any deep link that wasn't able to be shown,
       * null otherwise.
       */
      pendingSettingId_: {
        type: Number,
        value: null,
      },

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

  static get observers() {
    return [
      'deviceStateChanged_(deviceState)',
      'onAlwaysOnVpnChanged_(alwaysOnVpnMode_, alwaysOnVpnService_)',
    ];
  }

  defaultNetwork: OncMojo.NetworkStateProperties|null|undefined;
  deviceState: OncMojo.DeviceStateProperties|undefined;
  globalPolicy: GlobalPolicy|undefined;
  isBuiltInVpnManagementBlocked: boolean;
  isCellularSetupActive: boolean;
  isConnectedToNonCellularNetwork: boolean;
  showSpinner: boolean;
  tetherDeviceState: OncMojo.DeviceStateProperties|undefined;
  vpnProviders: VpnProvider[];
  private alwaysOnVpnMode_: AlwaysOnVpnMode|undefined;
  private alwaysOnVpnService_: string|undefined;
  private browserProxy_: InternetPageBrowserProxy;
  private hasCompletedScanSinceLastEnabled_: boolean;
  private isInstantHotspotRebrandEnabled_: boolean;
  private isManaged_: boolean;
  private isShowingTether_: boolean;
  private isShowingVpn_: boolean;
  private networkConfig_: CrosNetworkConfigInterface;
  private networkStateList_: OncMojo.NetworkStateProperties[];
  private notificationsDisabledDeviceNames_: string[];
  private pendingSettingId_: Setting|null;
  private scanIntervalId_: number|null;
  private showTechnologyBadge_: boolean;
  private thirdPartyVpns_: Record<string, OncMojo.NetworkStateProperties[]>;
  private vpnIsEnabled_: boolean;

  constructor() {
    super();

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

    this.scanIntervalId_ = null;
    this.browserProxy_ = InternetPageBrowserProxyImpl.getInstance();
    this.networkConfig_ =
        MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
  }

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

    this.browserProxy_.setGmsCoreNotificationsDisabledDeviceNamesCallback(
        (notificationsDisabledDeviceNames) => {
          this.notificationsDisabledDeviceNames_ =
              notificationsDisabledDeviceNames;
        });
    this.browserProxy_.requestGmsCoreNotificationsDisabledDeviceNames();

    this.addFocusConfig(routes.KNOWN_NETWORKS, '#knownNetworksSubpageButton');
  }

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

    this.stopScanning_();
  }

  /**
   * Overridden from DeepLinkingMixin.
   */
  override beforeDeepLinkAttempt(settingId: Setting): boolean {
    if (settingId === Setting.kAddESimNetwork) {
      afterNextRender(this, () => {
        const deepLinkElement =
            this.shadowRoot!.querySelector(
                                'cellular-networks-list')!.getAddEsimButton();
        if (!deepLinkElement || deepLinkElement.hidden) {
          console.warn(`Element with deep link id ${settingId} not focusable.`);
          return;
        }
        this.showDeepLinkElement(deepLinkElement);
      });
      return false;
    }

    if (settingId === Setting.kInstantTetheringOnOff) {
      // Wait for element to load.
      afterNextRender(this, () => {
        // If both Cellular and Instant Tethering are enabled, we show a special
        // toggle for Instant Tethering. If it exists, deep link to it.
        const tetherEnabled =
            this.shadowRoot!.querySelector<HTMLElement>('#tetherEnabledButton');
        if (tetherEnabled) {
          this.showDeepLinkElement(tetherEnabled);
          return;
        }
        // Otherwise, the device does not support Cellular and Instant Tethering
        // on/off is controlled by the top-level "Mobile data" toggle instead.
        const deviceEnabled =
            this.shadowRoot!.querySelector<HTMLElement>('#deviceEnabledButton');
        if (deviceEnabled) {
          this.showDeepLinkElement(deviceEnabled);
          return;
        }
        console.warn(`Element with deep link id ${settingId} not focusable.`);
      });
      // Stop deep link attempt since we completed it manually.
      return false;
    }
    return true;
  }

  /**
   * RouteObserverMixin override
   */
  override currentRouteChanged(newRoute: Route, oldRoute?: Route): void {
    if (newRoute !== this.route) {
      this.stopScanning_();
      return;
    }
    this.init();
    super.currentRouteChanged(newRoute, oldRoute);

    this.attemptDeepLink().then(result => {
      if (!result.deepLinkShown && result.pendingSettingId) {
        // Store any deep link settingId that wasn't shown so we can try again
        // in getNetworkStateList_.
        this.pendingSettingId_ = result.pendingSettingId;
      }
    });
  }

  init(): void {
    // Clear any stale data.
    this.networkStateList_ = [];
    this.thirdPartyVpns_ = {};
    this.hasCompletedScanSinceLastEnabled_ = false;
    this.showSpinner = false;

    // Request the list of networks and start scanning if necessary.
    this.getNetworkStateList_();
    this.updateScanning_();

    // Get always-on VPN configuration.
    this.updateAlwaysOnVpnPreferences_();
  }

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

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

  /** NetworkListenerBehavior override */
  override onVpnProvidersChanged(): void {
    if (this.deviceState!.type !== NetworkType.kVPN) {
      return;
    }
    this.getNetworkStateList_();
  }

  private deviceStateChanged_(): void {
    if (this.deviceState) {
      // Set |vpnIsEnabled_| to be used for VPN special cases.
      if (this.deviceState.type === NetworkType.kVPN) {
        this.vpnIsEnabled_ =
            this.deviceState.deviceState === DeviceStateType.kEnabled;
      }

      // A scan has completed if the spinner was active (i.e., scanning was
      // active) and the device is no longer scanning.
      this.hasCompletedScanSinceLastEnabled_ = this.showSpinner &&
          !this.deviceState.scanning &&
          this.deviceState.deviceState === DeviceStateType.kEnabled;

      // If the cellular network list is showing and currently inhibited, there
      // is a separate spinner that shows in the CellularNetworkList.
      if (this.shouldShowCellularNetworkList_() && this.isDeviceInhibited_()) {
        this.showSpinner = false;
      } else {
        this.showSpinner = !!this.deviceState.scanning;
      }
    }

    // Scans should only be triggered by the "networks" subpage.
    if (Router.getInstance().currentRoute !== routes.INTERNET_NETWORKS) {
      this.stopScanning_();
      return;
    }

    this.getNetworkStateList_();
    this.updateScanning_();
  }

  private updateScanning_(): void {
    if (!this.deviceState) {
      return;
    }

    if (this.shouldStartScan_()) {
      this.startScanning_();
      return;
    }
  }

  private shouldStartScan_(): boolean {
    assert(this.deviceState);
    // Scans should be kicked off from the Wi-Fi networks subpage.
    if (this.deviceState.type === NetworkType.kWiFi) {
      return true;
    }

    // Scans should be kicked off from the new Instant Hotspot page.
    return this.deviceState.type === NetworkType.kTether ||
        (this.deviceState.type === NetworkType.kCellular &&
         !!this.tetherDeviceState && !this.isInstantHotspotRebrandEnabled_);
  }

  private startScanning_(): void {
    if (this.scanIntervalId_ !== null) {
      return;
    }
    const INTERVAL_MS = 10 * 1000;
    let type = this.deviceState!.type;
    if (!this.isInstantHotspotRebrandEnabled_ &&
        type === NetworkType.kCellular && this.tetherDeviceState) {
      // Only request tether scan. Cellular scan is disruptive and should
      // only be triggered by explicit user action.
      type = NetworkType.kTether;
    }
    this.networkConfig_.requestNetworkScan(type);
    this.scanIntervalId_ = window.setInterval(() => {
      this.networkConfig_.requestNetworkScan(type);
    }, INTERVAL_MS);
  }

  private stopScanning_(): void {
    if (this.scanIntervalId_ === null) {
      return;
    }
    window.clearInterval(this.scanIntervalId_);
    this.scanIntervalId_ = null;
  }

  private async getNetworkStateList_(): Promise<void> {
    if (!this.deviceState) {
      return;
    }
    const filter = {
      filter: FilterType.kVisible,
      limit: NO_LIMIT,
      networkType: this.deviceState.type,
    };
    const response = await this.networkConfig_.getNetworkStateList(filter);
    await this.onGetNetworks_(response.result);

    // Check if we have yet to focus a deep-linked element.
    if (!this.pendingSettingId_) {
      return;
    }

    const result = await this.showDeepLink(this.pendingSettingId_);
    if (result.deepLinkShown) {
      this.pendingSettingId_ = null;
    }
  }

  private async onGetNetworks_(networkStates: OncMojo.NetworkStateProperties[]):
      Promise<void> {
    if (!this.deviceState) {
      // Edge case when device states change before this callback.
      return;
    }

    // For the Cellular/Mobile subpage, also request Tether networks.
    if (!this.isInstantHotspotRebrandEnabled_ &&
        this.deviceState.type === NetworkType.kCellular &&
        this.tetherDeviceState) {
      const filter = {
        filter: FilterType.kVisible,
        limit: NO_LIMIT,
        networkType: NetworkType.kTether,
      };
      const response = await this.networkConfig_.getNetworkStateList(filter);
      this.set('networkStateList_', networkStates.concat(response.result));
      return;
    }

    // For VPNs, separate out third party (Extension + Arc) VPNs.
    if (this.deviceState.type === NetworkType.kVPN) {
      const builtinNetworkStates: OncMojo.NetworkStateProperties[] = [];
      const thirdPartyVpns:
          Record<string, OncMojo.NetworkStateProperties[]> = {};
      networkStates.forEach(state => {
        assert(state.type === NetworkType.kVPN && state.typeState.vpn);
        switch (state.typeState.vpn.type) {
          case VpnType.kIKEv2:
          case VpnType.kL2TPIPsec:
          case VpnType.kOpenVPN:
          case VpnType.kWireGuard:
            builtinNetworkStates.push(state);
            break;
          // @ts-expect-error Fallthrough case in switch
          case VpnType.kArc:
            // Only show connected Arc VPNs.
            if (!OncMojo.connectionStateIsConnected(state.connectionState)) {
              break;
            }
            // Otherwise Arc VPNs are treated the same as Extension VPNs.
          case VpnType.kExtension:
            const providerId = state.typeState.vpn.providerId;
            thirdPartyVpns[providerId] = thirdPartyVpns[providerId] || [];
            thirdPartyVpns[providerId].push(state);
            break;
        }
      });
      networkStates = builtinNetworkStates;
      this.thirdPartyVpns_ = thirdPartyVpns;
    }

    this.set('networkStateList_', networkStates);
  }

  /**
   * Returns an ordered list of VPN providers for all third party VPNs and any
   * other known providers.
   */
  private getVpnProviders_(
      vpnProviders: VpnProvider[],
      thirdPartyVpns: Record<string, OncMojo.NetworkStateProperties[]>):
      VpnProvider[] {
    // First add providers for configured thirdPartyVpns. This list will
    // generally be empty or small.
    const configuredProviders = [];
    for (const vpnList of Object.values(thirdPartyVpns)) {
      assert(vpnList.length > 0);
      // All vpns in the list will have the same type and provider id.
      const vpn = castExists(vpnList[0].typeState.vpn);
      const provider = {
        type: vpn.type,
        providerId: vpn.providerId,
        providerName: vpn.providerName || vpn.providerId,
        appId: '',
        lastLaunchTime: {internalValue: BigInt(0)},
      };
      configuredProviders.push(provider);
    }
    // Next update or append known third party providers.
    const unconfiguredProviders = [];
    for (const provider of vpnProviders) {
      const idx: number = configuredProviders.findIndex(
          p => p.providerId === provider.providerId);
      if (idx >= 0) {
        configuredProviders[idx] = provider;
      } else {
        unconfiguredProviders.push(provider);
      }
    }
    return configuredProviders.concat(unconfiguredProviders);
  }

  /**
   * @return True if the device is enabled or if it is a VPN.
   *     Note: This function will always return true for VPN because VPNs can be
   *     disabled by policy only for built-in VPNs (OpenVPN & L2TP). So even
   *     when VPNs are disabled by policy; the VPN network summary item should
   *     still be visible and actionable to show details for other VPN
   *     providers.
   */
  private deviceIsEnabled_(deviceState: OncMojo.DeviceStateProperties|
                           undefined): boolean {
    if (OncMojo.deviceIsFlashing(deviceState)) {
      return false;
    }

    return !!deviceState &&
        (deviceState.type === NetworkType.kVPN ||
         deviceState.deviceState === DeviceStateType.kEnabled);
  }

  private getOffOnString_(
      deviceState: OncMojo.DeviceStateProperties|undefined, onstr: string,
      offstr: string): string {
    return this.deviceIsEnabled_(deviceState) ? onstr : offstr;
  }

  private enableToggleIsVisible_(deviceState: OncMojo.DeviceStateProperties|
                                 undefined): boolean {
    return !!deviceState && deviceState.type !== NetworkType.kEthernet &&
        deviceState.type !== NetworkType.kVPN &&
        (!this.isInstantHotspotRebrandEnabled_ ||
         deviceState.type !== NetworkType.kTether);
  }

  private enableToggleIsEnabled_(deviceState: OncMojo.DeviceStateProperties|
                                 undefined): boolean {
    if (!deviceState) {
      return false;
    }
    if (deviceState.deviceState === DeviceStateType.kProhibited) {
      return false;
    }
    if (OncMojo.deviceStateIsIntermediate(deviceState.deviceState)) {
      return false;
    }
    if (OncMojo.deviceIsFlashing(deviceState)) {
      return false;
    }
    return !this.isDeviceInhibited_();
  }

  private isDeviceInhibited_(): boolean {
    if (!this.deviceState) {
      return false;
    }
    return OncMojo.deviceIsInhibited(this.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 getAddThirdPartyVpnA11yString_(provider: VpnProvider): string {
    return this.i18n('internetAddThirdPartyVPN', provider.providerName || '');
  }

  private allowAddConnection_(
      deviceState: OncMojo.DeviceStateProperties|undefined,
      globalPolicy: GlobalPolicy): boolean {
    if (!this.deviceIsEnabled_(deviceState)) {
      return false;
    }
    return globalPolicy && !globalPolicy.allowOnlyPolicyWifiNetworksToConnect;
  }

  private showAddWifiButton_(
      deviceState: OncMojo.DeviceStateProperties|undefined,
      globalPolicy: GlobalPolicy): boolean {
    if (!deviceState || deviceState.type !== NetworkType.kWiFi) {
      return false;
    }
    return this.allowAddConnection_(deviceState, globalPolicy);
  }

  private dispatchShowConfigEvent_(type: string): void {
    const event = new CustomEvent('show-config', {
      bubbles: true,
      composed: true,
      detail: {type},
    });
    this.dispatchEvent(event);
  }

  private onAddWifiButtonClick_(): void {
    assert(this.deviceState, 'Device state is falsey - Wifi expected.');
    const type = this.deviceState.type;
    assert(type === NetworkType.kWiFi, 'Wifi type expected.');
    this.dispatchShowConfigEvent_(OncMojo.getNetworkTypeString(type));
  }

  private onAddVpnButtonClick_(): void {
    assert(this.deviceState, 'Device state is falsey - VPN expected.');
    const type = this.deviceState.type;
    assert(type === NetworkType.kVPN, 'VPN type expected.');
    this.dispatchShowConfigEvent_(OncMojo.getNetworkTypeString(type));
  }

  private onAddThirdPartyVpnClick_(event: DomRepeatEvent<VpnProvider>): void {
    const provider = event.model.item;
    this.browserProxy_.addThirdPartyVpn(provider.appId);
    // TODO(b/282233232) recordSettingChange() for adding third party VPN.
  }

  private knownNetworksIsVisible_(deviceState: OncMojo.DeviceStateProperties|
                                  undefined): boolean {
    return !!deviceState && deviceState.type === NetworkType.kWiFi;
  }

  /**
   * Event triggered when the known networks button is clicked.
   */
  private onKnownNetworksClick_(): void {
    assert(this.deviceState?.type === NetworkType.kWiFi);
    const showKnownNetworksEvent = new CustomEvent('show-known-networks', {
      bubbles: true,
      composed: true,
      detail: this.deviceState.type,
    });
    this.dispatchEvent(showKnownNetworksEvent);
  }

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

  private getThirdPartyVpnNetworks_(
      thirdPartyVpns: Record<string, OncMojo.NetworkStateProperties[]>,
      provider: VpnProvider): OncMojo.NetworkStateProperties[] {
    return thirdPartyVpns[provider.providerId] || [];
  }

  private haveThirdPartyVpnNetwork_(
      thirdPartyVpns: Record<string, OncMojo.NetworkStateProperties[]>,
      provider: VpnProvider): boolean {
    const list = this.getThirdPartyVpnNetworks_(thirdPartyVpns, provider);
    return !!list.length;
  }

  /**
   * Event triggered when a network list item is selected.
   */
  private onNetworkSelected_(e: CustomEvent<OncMojo.NetworkStateProperties>):
      void {
    assert(this.globalPolicy);
    assert(this.defaultNetwork !== undefined);
    const networkState = e.detail;
    (e.target as HTMLElement).blur();
    if (this.canAttemptConnection_(networkState)) {
      const networkConnectEvent = new CustomEvent('network-connect', {
        bubbles: true,
        composed: true,
        detail: {networkState},
      });
      this.dispatchEvent(networkConnectEvent);
      // TODO(b/282233232) recordSettingChange() for connecting to network.
      return;
    }

    const showDetailEvent = new CustomEvent('show-detail', {
      bubbles: true,
      composed: true,
      detail: networkState,
    });
    this.dispatchEvent(showDetailEvent);
  }

  private isBlockedByPolicy_(state: OncMojo.NetworkStateProperties): boolean {
    if (state.type !== NetworkType.kWiFi &&
        state.type !== NetworkType.kCellular) {
      return false;
    }
    if (this.isPolicySource(state.source) || !this.globalPolicy) {
      return false;
    }

    if (state.type === NetworkType.kCellular) {
      return !!this.globalPolicy.allowOnlyPolicyCellularNetworks;
    }

    return !!this.globalPolicy.allowOnlyPolicyWifiNetworksToConnect ||
        (!!this.globalPolicy.allowOnlyPolicyWifiNetworksToConnectIfAvailable &&
         !!this.deviceState && !!this.deviceState.managedNetworkAvailable) ||
        (!!this.globalPolicy.blockedHexSsids &&
         this.globalPolicy.blockedHexSsids.includes(
             state.typeState.wifi!.hexSsid));
  }

  /**
   * Determines whether or not it is possible to attempt a connection to the
   * provided network (e.g., whether it's possible to connect or configure the
   * network for connection).
   */
  private canAttemptConnection_(state: OncMojo.NetworkStateProperties):
      boolean {
    if (state.connectionState !== ConnectionStateType.kNotConnected) {
      return false;
    }

    if (this.isBlockedByPolicy_(state)) {
      return false;
    }

    // VPNs can only be connected if there is an existing network connection to
    // use with the VPN.
    if (state.type === NetworkType.kVPN &&
        (!this.defaultNetwork ||
         !OncMojo.connectionStateIsConnected(
             this.defaultNetwork.connectionState))) {
      return false;
    }

    // Locked SIM profiles must be unlocked before a connection can occur.
    if (state.type === NetworkType.kCellular &&
        state.typeState.cellular!.simLocked) {
      return false;
    }

    return true;
  }

  private matchesType_(
      typeString: string, device: OncMojo.DeviceStateProperties): boolean {
    return !!device &&
        device.type === OncMojo.getNetworkTypeFromString(typeString);
  }

  private shouldShowNetworkList_(
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    if (this.shouldShowCellularNetworkList_()) {
      return false;
    }

    if (!!this.deviceState && this.deviceState.type === NetworkType.kVPN) {
      return this.shouldShowVpnList_();
    }
    return networkStateList.length > 0;
  }

  /**
   * @return True if native VPN is not disabled by policy and there
   *     are more than one VPN network configured.
   */
  private shouldShowVpnList_(): boolean {
    return this.vpnIsEnabled_ && this.networkStateList_.length > 0;
  }

  private shouldShowCellularNetworkList_(): boolean {
    // Only shown if the currently-active subpage is for Cellular networks.
    return !!this.deviceState &&
        this.deviceState.type === NetworkType.kCellular;
  }

  private shouldShowBluetoothDisabledTetherErrorMessage_(
      deviceState: OncMojo.DeviceStateProperties|undefined): boolean {
    return this.isInstantHotspotRebrandEnabled_ && !!deviceState &&
        deviceState.type === NetworkType.kTether &&
        deviceState.deviceState === DeviceStateType.kUninitialized;
  }

  private hideNoNetworksMessage_(
      networkStateList: OncMojo.NetworkStateProperties[]): boolean {
    return this.shouldShowCellularNetworkList_() ||
        this.shouldShowNetworkList_(networkStateList);
  }

  private getNoNetworksInnerHtml_(
      deviceState: OncMojo.DeviceStateProperties,
      _tetherDeviceState: OncMojo.DeviceStateProperties|undefined): string {
    const type = deviceState.type;
    if (type === NetworkType.kTether && this.isInstantHotspotRebrandEnabled_) {
      return this.i18n('internetNoTetherHosts');
    }

    if (!this.isInstantHotspotRebrandEnabled_ &&
        (type === NetworkType.kCellular && this.tetherDeviceState ||
         type === NetworkType.kTether)) {
      return this.i18nAdvanced('internetNoNetworksMobileData').toString();
    }

    if (type === NetworkType.kVPN) {
      return this.i18n('internetNoNetworks');
    }

    // If a scan has not yet completed since the device was last enabled, it may
    // be the case that scan results are still in the process of arriving, so
    // display a message stating that scanning is in progress. If a scan has
    // already completed and there are still no networks present, this implies
    // that there has been sufficient time to find a network, so display a
    // messages stating that there are no networks. See https://crbug.com/974169
    // for more details.
    return this.hasCompletedScanSinceLastEnabled_ ?
        this.i18n('internetNoNetworks') :
        this.i18n('networkScanningLabel');
  }

  private getBluetoothDisabledErrorMessageForTether_(): string {
    return this.i18n('tetherEnableBluetooth');
  }

  private showGmsCoreNotificationsSection_(notificationsDisabledDeviceNames:
                                               string[]): boolean {
    return notificationsDisabledDeviceNames.length > 0;
  }

  private getGmsCoreNotificationsDevicesString_(
      notificationsDisabledDeviceNames: string[]): string {
    if (notificationsDisabledDeviceNames.length === 1) {
      return this.i18n(
          'gmscoreNotificationsOneDeviceSubtitle',
          notificationsDisabledDeviceNames[0]);
    }

    if (notificationsDisabledDeviceNames.length === 2) {
      return this.i18n(
          'gmscoreNotificationsTwoDevicesSubtitle',
          notificationsDisabledDeviceNames[0],
          notificationsDisabledDeviceNames[1]);
    }

    return this.i18n('gmscoreNotificationsManyDevicesSubtitle');
  }

  private computeIsShowingVpn_(): boolean {
    if (!this.deviceState) {
      return false;
    }
    return this.matchesType_(
        OncMojo.getNetworkTypeString(NetworkType.kVPN), this.deviceState);
  }

  private computeIsShowingTether_(): boolean {
    return !!this.deviceState &&
        this.matchesType_(
            OncMojo.getNetworkTypeString(NetworkType.kTether),
            this.deviceState);
  }

  /**
   * Tells when VPN preferences section should be displayed. It is
   * displayed when the preferences are applicable to the current device.
   */
  private shouldShowVpnPreferences_(): boolean {
    if (!this.deviceState) {
      return false;
    }
    // For now the section only contain always-on VPN settings. It should not be
    // displayed on managed devices while the legacy always-on VPN based on ARC
    // is not replaced/extended by the new implementation.
    return !this.isManaged_ && this.isShowingVpn_;
  }

  /**
   * Tells whether the Tether notification control should be displayed. It is
   * displayed when instant-hotspot-rebrand is enabled and there are Tether
   * networks.
   */
  private shouldShowTetherNotificationControl_(
      deviceState: OncMojo.DeviceStateProperties|undefined): boolean {
    return !!deviceState && deviceState.type === NetworkType.kTether &&
        this.isInstantHotspotRebrandEnabled_;
  }

  /*
   * Says whether header for the Tether network list should be displayed.
   * Returns true if the rebrand is enabled and the device state is Tether
   */
  private shouldShowTetherDeviceListHeader_(deviceState:
                                                OncMojo.DeviceStateProperties|
                                            undefined): boolean {
    return !!deviceState && deviceState.type === NetworkType.kTether &&
        this.isInstantHotspotRebrandEnabled_;
  }

  /**
   * Generates the list of VPN services available for always-on. It keeps from
   * the network list only the supported technologies.
   */
  private getAlwaysOnVpnNetworks_(): OncMojo.NetworkStateProperties[] {
    if (!this.deviceState || this.deviceState.type !== NetworkType.kVPN) {
      return [];
    }

    const alwaysOnVpnList = this.networkStateList_.slice();
    for (const vpnList of Object.values(this.thirdPartyVpns_)) {
      assert(vpnList.length > 0);
      // Exclude incompatible VPN technologies:
      // - TODO(b/188864779): ARC VPNs are not supported yet,
      // - Chrome VPN apps are deprecated and incompatible with lockdown mode
      //   (see b/206910855).
      if (vpnList[0].typeState.vpn!.type === VpnType.kArc ||
          vpnList[0].typeState.vpn!.type === VpnType.kExtension) {
        continue;
      }
      alwaysOnVpnList.push(...vpnList);
    }

    return alwaysOnVpnList;
  }

  /**
   * Fetches the always-on VPN configuration from network config.
   */
  private async updateAlwaysOnVpnPreferences_(): Promise<void> {
    if (!this.deviceState || this.deviceState.type !== NetworkType.kVPN) {
      return;
    }

    const result = await this.networkConfig_.getAlwaysOnVpn();
    this.alwaysOnVpnMode_ = result.properties.mode;
    this.alwaysOnVpnService_ = result.properties.serviceGuid;
  }

  /**
   * Handles a change in |alwaysOnVpnMode_| or |alwaysOnVpnService_|
   * triggered via the observer.
   */
  private onAlwaysOnVpnChanged_(): void {
    if (this.alwaysOnVpnMode_ === undefined ||
        this.alwaysOnVpnService_ === undefined) {
      return;
    }

    const properties: AlwaysOnVpnProperties = {
      mode: this.alwaysOnVpnMode_,
      serviceGuid: this.alwaysOnVpnService_,
    };
    this.networkConfig_.setAlwaysOnVpn(properties);
  }
}

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

customElements.define(
    SettingsInternetSubpageElement.is, SettingsInternetSubpageElement);