chromium/chrome/browser/resources/ash/settings/internet_page/internet_page.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
 * 'settings-internet-page' is the settings page containing internet
 * settings.
 */

import 'chrome://resources/ash/common/cellular_setup/cellular_setup_icons.html.js';
import 'chrome://resources/ash/common/network/sim_lock_dialogs.js';
import 'chrome://resources/ash/common/cr_elements/cr_expand_button/cr_expand_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_toast/cr_toast.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_policy_indicator.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_tooltip_icon.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/paper-tooltip/paper-tooltip.js';
import '/shared/settings/prefs/prefs.js';
import '../settings_shared.css.js';
import '../os_settings_page/os_settings_animated_pages.js';
import '../os_settings_page/os_settings_subpage.js';
import '../os_settings_page/settings_card.js';
import '../os_settings_icons.css.js';
import './cellular_setup_dialog.js';
import './esim_rename_dialog.js';
import './esim_remove_profile_dialog.js';
import './hotspot_config_dialog.js';
import './internet_config.js';
import './internet_detail_menu.js';
import './network_summary.js';

import {PrefsMixin, PrefsMixinInterface} from '/shared/settings/prefs/prefs_mixin.js';
import {CellularSetupPageName} from 'chrome://resources/ash/common/cellular_setup/cellular_types.js';
import {getNumESimProfiles} from 'chrome://resources/ash/common/cellular_setup/esim_manager_utils.js';
import {PasspointSubscription} from 'chrome://resources/ash/common/connectivity/passpoint.mojom-webui.js';
import {CrActionMenuElement} from 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js';
import {CrToastElement} from 'chrome://resources/ash/common/cr_elements/cr_toast/cr_toast.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 {HotspotInfo, HotspotState} from 'chrome://resources/ash/common/hotspot/cros_hotspot_config.mojom-webui.js';
import {hasActiveCellularNetwork, isConnectedToNonCellularNetwork} from 'chrome://resources/ash/common/network/cellular_utils.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 {CrosNetworkConfigInterface, GlobalPolicy, NetworkStateProperties, StartConnectResult, VpnProvider} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {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 {Section} from '../mojom-webui/routes.mojom-webui.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, Router, routes} from '../router.js';

import {ApnSubpageElement} from './apn_subpage.js';
import {InternetConfigElement} from './internet_config.js';
import {getTemplate} from './internet_page.html.js';
import {InternetPageBrowserProxy, InternetPageBrowserProxyImpl} from './internet_page_browser_proxy.js';

const ESIM_PROFILE_LIMIT = 5;

declare global {
  interface HTMLElementEventMap {
    'device-enabled-toggled':
        CustomEvent<{enabled: boolean, type: NetworkType}>;
    'network-connect': CustomEvent<{
      networkState: OncMojo.NetworkStateProperties,
      bypassConnectionDialog: boolean|undefined,
    }>;
    'show-cellular-setup': CustomEvent<{pageName: CellularSetupPageName}>;
    'show-config':
        CustomEvent<{type: string, guid: string|null, name: string|null}>;
    'show-detail': CustomEvent<OncMojo.NetworkStateProperties>;
    'show-error-toast': CustomEvent<string>;
    'show-esim-profile-rename-dialog':
        CustomEvent<{networkState: NetworkStateProperties}>;
    'show-esim-remove-profile-dialog':
        CustomEvent<{networkState: NetworkStateProperties}>;
    'show-known-networks': CustomEvent<NetworkType>;
    'show-networks': CustomEvent<NetworkType>;
    'show-passpoint-detail': CustomEvent<PasspointSubscription>;
  }
}

export interface SettingsInternetPageElement {
  $: {
    apnDotsMenu: CrActionMenuElement,
    errorToast: CrToastElement,
    errorToastMessage: HTMLElement,
  };
}

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

export class SettingsInternetPageElement extends
    SettingsInternetPageElementBase {
  static get is() {
    return 'settings-internet-page' as const;
  }

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

  static get properties() {
    return {
      section_: {
        type: Number,
        value: Section.kNetwork,
        readOnly: true,
      },

      /**
       * The device state for each network device type, keyed by NetworkType.
       * Set by network-summary.
       */
      deviceStates: {
        type: Object,
        notify: true,
        observer: 'onDeviceStatesChanged_',
      },

      /**
       * Highest priority connected network or null. Set by network-summary.
       */
      defaultNetwork: {
        type: Object,
        notify: true,
      },

      /**
       * Hotspot information. Set by network-summary.
       */
      hotspotInfo: {
        type: Object,
        notify: true,
      },

      /**
       * Set by internet-subpage. Controls spinner visibility in subpage header.
       */
      showSpinner_: Boolean,

      /**
       * The network type for the networks subpage when shown.
       */
      subpageType_: Number,

      /**
       * The network type for the known networks subpage when shown.
       */
      knownNetworksType_: Number,

      /**
       * Whether the 'Add connection' section is expanded.
       */
      addConnectionExpanded_: {
        type: Boolean,
        value: false,
      },

      /**
       * True if VPN is prohibited by policy.
       */
      vpnIsProhibited_: {
        type: Boolean,
        value: false,
      },

      /**
       * True if VPN is prohibited by policy, or an always-on Android VPN is
       * enforced by policy and users are prohibited by policy from manually
       * disconnecting from it.
       */
      isBuiltInVpnManagementBlocked_: {
        type: Boolean,
        computed: 'computeIsBuiltInVpnManagementBlocked_(vpnIsProhibited_,' +
            'prefs.vpn_config_allowed.*' +
            'prefs.arc.vpn.*,' +
            'prefs.arc.vpn.always_on.*)',
      },

      globalPolicy_: Object,

      /**
       * Whether a managed network is available in the visible network list.
       */
      managedNetworkAvailable: {
        type: Boolean,
        value: false,
      },

      /**
       * List of third party (Extension + Arc) VPN providers.
       */
      vpnProviders_: {
        type: Array,
        value() {
          return [];
        },
      },

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

      isApnRevampEnabled_: {
        type: Boolean,
        value() {
          return loadTimeData.valueExists('isApnRevampEnabled') &&
              loadTimeData.getBoolean('isApnRevampEnabled');
        },
      },

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


      /**
       * Page name, if defined, indicating that the next deviceStates update
       * should call attemptShowCellularSetupDialog_().
       */
      pendingShowCellularSetupDialogAttemptPageName_: {
        type: String,
        value: null,
      },

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

      /**
       * Name of cellular setup dialog page.
       */
      cellularSetupDialogPageName_: String,

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

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

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

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

      /** @private {boolean} */
      showHotspotConfigDialog_: {
        type: Boolean,
        value: false,
      },

      /**
       * Flag, if true, indicating that the next deviceStates update
       * should set showSimLockDialog_ to true.
       */
      pendingShowSimLockDialog_: {
        type: Boolean,
        value: false,
      },

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

      isProviderLocked_: {
        type: Boolean,
        computed: 'showProviderLocked_(subpageType_, deviceStates)',
      },

      isDeviceUpdating_: {
        type: Boolean,
        computed: 'showDeviceUpdating_(subpageType_, deviceStates)',
      },

      /**
       * eSIM network used in internet detail menu.
       */
      eSimNetworkState_: {
        type: Object,
        value: '',
      },

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

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

      isApnRevampAndAllowApnModificationPolicyEnabled_: {
        type: Boolean,
        value() {
          return loadTimeData.valueExists(
                     'isApnRevampAndAllowApnModificationPolicyEnabled') &&
              loadTimeData.getBoolean(
                  'isApnRevampAndAllowApnModificationPolicyEnabled');
        },
      },

      /**
       * Whether the 'Add custom APN' button is disabled.
       */
      isNumCustomApnsLimitReached_: {
        type: Boolean,
      },

      /**
       * Passpoint subscription set by show-passpoint-detail.
       */
      passpointSubscription_: {
        type: Object,
        notify: true,
      },

      /**
       * The position of the tooltips displayed by the APN menu. This can be
       * 'right' if the language is RTL. This ensures the tooltip doesn't get
       * cut off in RTL languages (b/335486874).
       */
      apnMenuTooltipsPosition_: {
        type: String,
        value: 'left',
      },
    };
  }

  defaultNetwork: OncMojo.NetworkStateProperties|null|undefined;
  deviceStates: Record<string, OncMojo.DeviceStateProperties>|undefined;
  hotspotInfo: HotspotInfo|undefined;
  managedNetworkAvailable: boolean;
  private addConnectionExpanded_: boolean;
  private browserProxy_: InternetPageBrowserProxy;
  private cellularSetupDialogPageName_: CellularSetupPageName|null;
  private detailType_: NetworkType|null;
  private errorToastMessage_: string;
  private eSimNetworkState_: NetworkStateProperties;
  private globalPolicy_: GlobalPolicy|undefined;
  private hasActiveCellularNetwork_: boolean;
  private isConnectedToNonCellularNetwork_: boolean;
  private isNumCustomApnsLimitReached_: boolean;
  private isInstantHotspotRebrandEnabled_: boolean;
  private isApnRevampAndAllowApnModificationPolicyEnabled_: boolean;
  private isBuiltInVpnManagementBlocked_: boolean;
  private knownNetworksType_: NetworkType;
  private networkConfig_: CrosNetworkConfigInterface;
  private passpointSubscription_: PasspointSubscription|undefined;
  private pendingShowCellularSetupDialogAttemptPageName_: CellularSetupPageName|
      null;
  private pendingShowSimLockDialog_: boolean;
  private section_: Section;
  private showCellularSetupDialog_: boolean;
  private showESimProfileRenameDialog_: boolean;
  private showESimRemoveProfileDialog_: boolean;
  private showHotspotConfigDialog_: boolean;
  private showInternetConfig_: boolean;
  private showSimLockDialog_: boolean;
  private isProviderLocked_: boolean;
  private isDeviceUpdating_: boolean;
  private showSpinner_: boolean;
  private subpageType_: NetworkType;
  private vpnIsProhibited_: boolean;
  private vpnProviders_: VpnProvider[];
  private apnMenuTooltipsPosition_: string;

  constructor() {
    super();

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

    /**
     * Type of last detail page visited
     */
    this.detailType_ = null;

    this.browserProxy_ = InternetPageBrowserProxyImpl.getInstance();

    this.networkConfig_ =
        MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
  }

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

    this.addEventListener('device-enabled-toggled', (event) => {
      this.onDeviceEnabledToggled_(event);
    });
    this.addEventListener('network-connect', (event) => {
      this.onNetworkConnect_(event);
    });
    this.addEventListener('show-cellular-setup', (event) => {
      this.onShowCellularSetupDialog_(event);
    });
    this.addEventListener('show-config', (event) => {
      this.onShowConfig_(event);
    });
    this.addEventListener('show-detail', (event) => {
      this.onShowDetail_(event);
    });
    this.addEventListener('show-known-networks', (event) => {
      this.onShowKnownNetworks_(event);
    });
    this.addEventListener('show-networks', (event) => {
      this.onShowNetworks_(event);
    });
    this.addEventListener('show-esim-profile-rename-dialog', (event) => {
      this.onShowEsimProfileRenameDialog_(event);
    });
    this.addEventListener('show-esim-remove-profile-dialog', (event) => {
      this.onShowEsimRemoveProfileDialog_(event);
    });
    this.addEventListener('show-hotspot-config-dialog', () => {
      this.onShowHotspotConfigDialog_();
    });
    this.addEventListener('show-passpoint-detail', (event) => {
      this.onShowPasspointDetails_(event);
    });
    this.addEventListener('show-error-toast', (event) => {
      this.onShowErrorToast_(event);
    });

    [routes.INTERNET_NETWORKS,
     routes.NETWORK_DETAIL,
     routes.KNOWN_NETWORKS,
     routes.HOTSPOT_DETAIL,
     routes.APN,
     routes.PASSPOINT_DETAIL,
    ].forEach((route) => {
      this.addFocusConfig(route, () => {
        if (this.detailType_ !== null) {
          const rowForDetailType =
              this.shadowRoot!.querySelector('network-summary')!.getNetworkRow(
                  this.detailType_);

          // Note: It is possible that the row is no longer present in the DOM
          // (e.g., when a Cellular dongle is unplugged or when Instant
          // Tethering becomes unavailable due to the Bluetooth controller
          // disconnecting).
          if (rowForDetailType) {
            return rowForDetailType.shadowRoot!.querySelector<HTMLElement>(
                '.subpage-arrow');
          }
        }

        return null;
      });
    });
  }

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

    this.onPoliciesApplied(/*userhash=*/ '');
    this.onVpnProvidersChanged();
    this.onNetworkStateListChanged();

    const isRTL = window.getComputedStyle(this).direction === 'rtl';
    this.apnMenuTooltipsPosition_ = isRTL ? 'right' : 'left';
  }

  /**
   * Overridden from DeepLinkingMixin.
   */
  override beforeDeepLinkAttempt(settingId: Setting): boolean {
    // Manually show the deep links for settings nested within elements.
    let networkType: NetworkType|null = null;
    if (settingId === Setting.kWifiOnOff) {
      networkType = NetworkType.kWiFi;
    } else if (settingId === Setting.kMobileOnOff) {
      networkType = NetworkType.kCellular;
    } else {
      return true;
    }

    afterNextRender(this, () => {
      const networkRow =
          this.shadowRoot!.querySelector('network-summary')!.getNetworkRow(
              networkType!);
      if (networkRow) {
        const toggleEl = networkRow.getDeviceEnabledToggle();
        if (toggleEl) {
          this.showDeepLinkElement(toggleEl);
          return;
        }
      }
      console.warn(`Element with deep link id ${settingId} not focusable.`);
    });
    // Stop deep link attempt since we completed it manually.
    return false;
  }

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

    if (newRoute === this.route || newRoute === routes.APN) {
      // Show deep links for the internet page.
      this.attemptDeepLink();
    } else if (newRoute === routes.INTERNET_NETWORKS) {
      // Handle direct navigation to the networks page,
      // e.g. chrome://settings/internet/networks?type=WiFi
      const queryParams = Router.getInstance().getQueryParameters();
      const type = queryParams.get('type');
      if (type) {
        this.subpageType_ = OncMojo.getNetworkTypeFromString(type);
      }

      if (!oldRoute && queryParams.get('showCellularSetup') === 'true') {
        const pageName = queryParams.get('showPsimFlow') === 'true' ?
            CellularSetupPageName.PSIM_FLOW_UI :
            CellularSetupPageName.ESIM_FLOW_UI;
        // If the page just loaded, deviceStates will not be fully initialized
        // yet. Set pendingShowCellularSetupDialogAttemptPageName_ to indicate
        // showCellularSetupDialogAttempt_() should be called next deviceStates
        // update.
        this.pendingShowCellularSetupDialogAttemptPageName_ = pageName;
      }

      // If the page just loaded, deviceStates will not be fully initialized
      // yet. Set pendingShowSimLockDialog_ to indicate
      // showSimLockDialog_ should be set next deviceStates
      // update.
      this.pendingShowSimLockDialog_ = !oldRoute &&
          !!queryParams.get('showSimLockDialog') &&
          this.subpageType_ === NetworkType.kCellular;
    } else if (newRoute === routes.KNOWN_NETWORKS) {
      // Handle direct navigation to the known networks page,
      // e.g. chrome://settings/internet/knownNetworks?type=WiFi
      const queryParams = Router.getInstance().getQueryParameters();
      const type = queryParams.get('type');
      if (type) {
        this.knownNetworksType_ = OncMojo.getNetworkTypeFromString(type);
      } else {
        this.knownNetworksType_ = NetworkType.kWiFi;
      }
    }
  }

  /** NetworkListenerBehavior override */
  override onNetworkStateListChanged(): void {
    hasActiveCellularNetwork().then((hasActive) => {
      this.hasActiveCellularNetwork_ = hasActive;
    });
    this.updateIsConnectedToNonCellularNetwork_();
  }

  override async onVpnProvidersChanged(): Promise<void> {
    const response = await this.networkConfig_.getVpnProviders();
    const providers = response.providers;
    providers.sort(this.compareVpnProviders_);
    this.vpnProviders_ = providers;
  }

  override async onPoliciesApplied(_userhash: string): Promise<void> {
    const response = await this.networkConfig_.getGlobalPolicy();
    this.globalPolicy_ = response.result;
  }

  private async updateIsConnectedToNonCellularNetwork_(): Promise<boolean> {
    const isConnected = await isConnectedToNonCellularNetwork();
    this.isConnectedToNonCellularNetwork_ = isConnected;
    return isConnected;
  }

  /**
   * Event triggered by a device state enabled toggle.
   */
  private onDeviceEnabledToggled_(
      event: CustomEvent<{enabled: boolean, type: NetworkType}>): void {
    this.networkConfig_.setNetworkTypeEnabledState(
        event.detail.type, event.detail.enabled);
    // TODO(b/282233232) recordSettingChange() for enabling/disabling device.
  }

  private onShowConfig_(
      event: CustomEvent<{type: string, guid: string|null, name: string|null}>):
      void {
    const type = OncMojo.getNetworkTypeFromString(event.detail.type);
    if (!event.detail.guid) {
      // New configuration
      this.showConfig_(/* configAndConnect= */ true, type);
    } else {
      this.showConfig_(
          /* configAndConnect= */ false, type, event.detail.guid,
          event.detail.name);
    }
  }

  private onShowCellularSetupDialog_(
      event: CustomEvent<{pageName: CellularSetupPageName}>): void {
    this.attemptShowCellularSetupDialog_(event.detail.pageName);
  }

  /**
   * Opens the cellular setup dialog if pageName is PSIM_FLOW_UI, or if pageName
   * is ESIM_FLOW_UI and isConnectedToNonCellularNetwork_ is true. If
   * isConnectedToNonCellularNetwork_ is false, shows an error toast.
   */
  private attemptShowCellularSetupDialog_(pageName: CellularSetupPageName):
      void {
    const cellularDeviceState =
        this.getDeviceState_(NetworkType.kCellular, this.deviceStates);
    if (!cellularDeviceState ||
        cellularDeviceState.deviceState !== DeviceStateType.kEnabled) {
      this.showErrorToast_(this.i18n('eSimMobileDataNotEnabledErrorToast'));
      return;
    }

    if (pageName === CellularSetupPageName.PSIM_FLOW_UI) {
      this.showCellularSetupDialog_ = true;
      this.cellularSetupDialogPageName_ = pageName;
    } else {
      this.attemptShowEsimSetupDialog_();
    }
  }

  private async attemptShowEsimSetupDialog_(): Promise<void> {
    const numProfiles = await getNumESimProfiles();
    if (numProfiles >= ESIM_PROFILE_LIMIT) {
      this.showErrorToast_(
          this.i18n('eSimProfileLimitReachedErrorToast', ESIM_PROFILE_LIMIT));
      return;
    }

    // isConnectedToNonCellularNetwork_ may
    // not be fetched yet if the page just opened, fetch it
    // explicitly.
    const isConnected = await this.updateIsConnectedToNonCellularNetwork_();
    this.showCellularSetupDialog_ =
        isConnected || loadTimeData.getBoolean('bypassConnectivityCheck');
    if (!this.showCellularSetupDialog_) {
      this.showErrorToast_(this.i18n('eSimNoConnectionErrorToast'));
      return;
    }
    this.cellularSetupDialogPageName_ = CellularSetupPageName.ESIM_FLOW_UI;
  }

  private onShowErrorToast_(event: CustomEvent<string>): void {
    this.showErrorToast_(event.detail);
  }

  private showErrorToast_(message: string): void {
    this.errorToastMessage_ = message;
    this.$.errorToast.show();
  }

  private onCloseCellularSetupDialog_(): void {
    this.showCellularSetupDialog_ = false;
  }

  private showConfig_(
      configAndConnect: boolean, type: NetworkType, guid?: string|null,
      name?: string|null): void {
    assert(type !== NetworkType.kCellular && type !== NetworkType.kTether);
    if (this.showInternetConfig_) {
      return;
    }
    this.showInternetConfig_ = true;
    // Async call to ensure dialog is stamped.
    setTimeout(() => {
      const configDialog =
          castExists(this.shadowRoot!.querySelector<InternetConfigElement>(
              '#configDialog'));
      configDialog.type = OncMojo.getNetworkTypeString(type);
      configDialog.guid = guid || '';
      configDialog.name = name || '';
      configDialog.showConnect = configAndConnect;
      configDialog.open();
    });
  }

  private onInternetConfigClose_(): void {
    this.showInternetConfig_ = false;
  }

  private onShowDetail_(event: CustomEvent<OncMojo.NetworkStateProperties>):
      void {
    const networkState = event.detail;
    this.detailType_ = networkState.type;
    const params = new URLSearchParams();
    params.append('guid', networkState.guid);
    params.append('type', OncMojo.getNetworkTypeString(networkState.type));
    params.append(
        'name', OncMojo.getNetworkStateDisplayNameUnsafe(networkState));
    Router.getInstance().navigateTo(routes.NETWORK_DETAIL, params);
  }

  private onShowEsimProfileRenameDialog_(
      event: CustomEvent<{networkState: NetworkStateProperties}>): void {
    this.eSimNetworkState_ = event.detail.networkState;
    this.showESimProfileRenameDialog_ = true;
  }

  private onCloseEsimProfileRenameDialog_(): void {
    this.showESimProfileRenameDialog_ = false;
  }

  private onShowEsimRemoveProfileDialog_(
      event: CustomEvent<{networkState: NetworkStateProperties}>): void {
    this.eSimNetworkState_ = event.detail.networkState;
    this.showESimRemoveProfileDialog_ = true;
  }

  private onCloseEsimRemoveProfileDialog_(): void {
    this.showESimRemoveProfileDialog_ = false;
  }

  private onShowHotspotConfigDialog_(): void {
    this.showHotspotConfigDialog_ = true;
  }

  private onCloseHotspotConfigDialog_(): void {
    this.showHotspotConfigDialog_ = false;
  }

  private onShowNetworks_(event: CustomEvent<NetworkType>): void {
    this.showNetworksSubpage_(event.detail);
  }

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

  private showProviderLocked_(): boolean {
    if (this.subpageType_ !== NetworkType.kCellular) {
      return false;
    }
    // Check carrier lock status reported by carrier lock manager.
    const cellularDeviceState =
        this.getDeviceState_(NetworkType.kCellular, this.deviceStates);
    if (!cellularDeviceState || !cellularDeviceState.isCarrierLocked) {
      return false;
    }
    return true;
  }

  private showDeviceUpdating_(): boolean {
    if (this.subpageType_ !== NetworkType.kCellular) {
      return false;
    }
    const cellularDeviceState =
        this.getDeviceState_(NetworkType.kCellular, this.deviceStates);
    if (!cellularDeviceState || !cellularDeviceState.isFlashing) {
      return false;
    }
    return true;
  }

  private getDeviceState_(
      subpageType: NetworkType,
      deviceStates: Record<string, OncMojo.DeviceStateProperties>|
      undefined): OncMojo.DeviceStateProperties|undefined {
    if (subpageType === undefined) {
      return undefined;
    }
    // If both Tether and Cellular are enabled, use the Cellular device state
    // when directly navigating to the Tether page.
    if (subpageType === NetworkType.kTether &&
        this.deviceStates![NetworkType.kCellular] &&
        !this.isInstantHotspotRebrandEnabled_) {
      subpageType = NetworkType.kCellular;
    }
    return deviceStates![subpageType];
  }

  private getTetherDeviceState_(
      deviceStates: Record<string, OncMojo.DeviceStateProperties>|
      undefined): OncMojo.DeviceStateProperties|undefined {
    return deviceStates![NetworkType.kTether];
  }

  private onDeviceStatesChanged_(
      newValue: Record<string, OncMojo.DeviceStateProperties>|undefined,
      _oldValue: Record<string, OncMojo.DeviceStateProperties>|
      undefined): void {
    const wifiDeviceState = this.getDeviceState_(NetworkType.kWiFi, newValue);
    let managedNetworkAvailable = false;
    if (wifiDeviceState) {
      managedNetworkAvailable = !!wifiDeviceState.managedNetworkAvailable;
    }

    if (this.managedNetworkAvailable !== managedNetworkAvailable) {
      this.managedNetworkAvailable = managedNetworkAvailable;
    }

    assert(this.deviceStates);
    const vpn = this.deviceStates[NetworkType.kVPN];
    this.vpnIsProhibited_ =
        !!vpn && vpn.deviceState === DeviceStateType.kProhibited;

    if (this.detailType_ && !this.deviceStates[this.detailType_]) {
      // If the device type associated with the current network has been
      // removed (e.g., due to unplugging a Cellular dongle), the details page,
      // if visible, displays controls which are no longer functional. If this
      // case occurs, close the details page.
      const detailPage =
          this.shadowRoot!.querySelector('settings-internet-detail-subpage');
      if (detailPage) {
        detailPage.close();
      }
    }

    if (this.pendingShowCellularSetupDialogAttemptPageName_) {
      this.attemptShowCellularSetupDialog_(
          this.pendingShowCellularSetupDialogAttemptPageName_);
      this.pendingShowCellularSetupDialogAttemptPageName_ = null;
    }

    if (this.pendingShowSimLockDialog_) {
      this.showSimLockDialog_ = true;
      this.pendingShowSimLockDialog_ = false;
    }
  }

  private onShowKnownNetworks_(event: CustomEvent<NetworkType>): void {
    const type = event.detail;
    this.detailType_ = type;
    this.knownNetworksType_ = type;
    const params = new URLSearchParams();
    params.append('type', OncMojo.getNetworkTypeString(type));
    Router.getInstance().navigateTo(routes.KNOWN_NETWORKS, params);
  }

  private onAddWiFiClick_(): void {
    this.showConfig_(true /* configAndConnect */, NetworkType.kWiFi);
  }

  private onAddVpnClick_(): void {
    if (!this.isBuiltInVpnManagementBlocked_) {
      this.showConfig_(true /* configAndConnect */, NetworkType.kVPN);
    }
  }

  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 showNetworksSubpage_(type: NetworkType): void {
    this.detailType_ = type;
    const params = new URLSearchParams();
    params.append('type', OncMojo.getNetworkTypeString(type));
    this.subpageType_ = type;
    Router.getInstance().navigateTo(routes.INTERNET_NETWORKS, params);
  }

  private compareVpnProviders_(
      vpnProvider1: VpnProvider, vpnProvider2: VpnProvider): number {
    // Show Extension VPNs before Arc VPNs.
    if (vpnProvider1.type < vpnProvider2.type) {
      return -1;
    }
    if (vpnProvider1.type > vpnProvider2.type) {
      return 1;
    }
    // Show VPNs of the same type by lastLaunchTime.
    if (vpnProvider1.lastLaunchTime.internalValue >
        vpnProvider2.lastLaunchTime.internalValue) {
      return -1;
    }
    if (vpnProvider1.lastLaunchTime.internalValue <
        vpnProvider2.lastLaunchTime.internalValue) {
      return 1;
    }
    return 0;
  }

  private wifiIsEnabled_(deviceStates: OncMojo.DeviceStateProperties[]):
      boolean {
    const wifi = deviceStates[NetworkType.kWiFi];
    return !!wifi && wifi.deviceState === DeviceStateType.kEnabled;
  }

  private shouldShowAddWiFiRow_(
      globalPolicy: GlobalPolicy, managedNetworkAvailable: boolean,
      deviceStates: OncMojo.DeviceStateProperties[]): boolean {
    return this.allowAddWiFiConnection_(
               globalPolicy, managedNetworkAvailable) &&
        this.wifiIsEnabled_(deviceStates);
  }

  private allowAddWiFiConnection_(
      globalPolicy: GlobalPolicy, managedNetworkAvailable: boolean): boolean {
    if (!globalPolicy) {
      return true;
    }

    return !globalPolicy.allowOnlyPolicyWifiNetworksToConnect &&
        (!globalPolicy.allowOnlyPolicyWifiNetworksToConnectIfAvailable ||
         !managedNetworkAvailable);
  }

  private allowAddConnection_(
      globalPolicy: GlobalPolicy, managedNetworkAvailable: boolean): boolean {
    if (!this.vpnIsProhibited_) {
      return true;
    }
    return this.allowAddWiFiConnection_(globalPolicy, managedNetworkAvailable);
  }

  private computeIsBuiltInVpnManagementBlocked_(): boolean {
    if (this.vpnIsProhibited_) {
      return true;
    }

    if (!this.prefs) {
      return false;
    }

    const isVpnConfigProhibited =
        this.prefs.vpn_config_allowed && !this.prefs.vpn_config_allowed.value;
    const hasAlwaysOnVpnActivated = this.prefs.arc && this.prefs.arc.vpn &&
        this.prefs.arc.vpn.always_on &&
        this.prefs.arc.vpn.always_on.vpn_package &&
        !!this.prefs.arc.vpn.always_on.vpn_package.value;
    return isVpnConfigProhibited && hasAlwaysOnVpnActivated;
  }

  private getAddThirdPartyVpnLabel_(provider: VpnProvider): string {
    return this.i18n('internetAddThirdPartyVPN', provider.providerName || '');
  }

  /**
   * Handles UI requests to connect to a network.
   * TODO(stevenjb): Handle Cellular activation, etc.
   */
  private async onNetworkConnect_(event: CustomEvent<{
    networkState: OncMojo.NetworkStateProperties,
    bypassConnectionDialog: boolean|undefined,
  }>): Promise<void> {
    const networkState = event.detail.networkState;
    const type = networkState.type;
    const displayName = OncMojo.getNetworkStateDisplayNameUnsafe(networkState);

    if (!event.detail.bypassConnectionDialog && type === NetworkType.kTether &&
        !networkState.typeState.tether!.hasConnectedToHost) {
      const params = new URLSearchParams();
      params.append('guid', networkState.guid);
      params.append('type', OncMojo.getNetworkTypeString(type));
      params.append('name', displayName);
      params.append('showConfigure', true.toString());

      Router.getInstance().navigateTo(routes.NETWORK_DETAIL, params);
      return;
    }

    if (OncMojo.networkTypeHasConfigurationFlow(type) &&
        (!OncMojo.isNetworkConnectable(networkState) ||
         !!networkState.errorState)) {
      this.showConfig_(
          true /* configAndConnect */, type, networkState.guid, displayName);
      return;
    }

    const response = await this.networkConfig_.startConnect(networkState.guid);
    switch (response.result) {
      case StartConnectResult.kSuccess:
        return;
      case StartConnectResult.kInvalidGuid:
      case StartConnectResult.kInvalidState:
      case StartConnectResult.kCanceled:
        // TODO(stevenjb/khorimoto): Consider handling these cases.
        return;
      case StartConnectResult.kNotConfigured:
        if (OncMojo.networkTypeHasConfigurationFlow(type)) {
          this.showConfig_(
              true /* configAndConnect */, type, networkState.guid,
              displayName);
        }
        return;
      case StartConnectResult.kBlocked:
        // This shouldn't happen, the UI should prevent this, fall through and
        // show the error.
      case StartConnectResult.kUnknown:
        console.warn(
            'startConnect failed for: ' + networkState.guid +
            ' Error: ' + response.message);
        return;
    }
    assertNotReached();
  }

  /** Opens the three dots menu. */
  private onApnMenuButtonClicked_(event: Event): void {
    const target = event.target as HTMLElement | null;
    if (!target) {
      return;
    }
    (this.$.apnDotsMenu).showAt((target));
  }

  private closeApnMenu_(): void {
    (this.$.apnDotsMenu).close();
  }

  /**
   * Handles UI requests to add new APN.
   */
  private onCreateCustomApnClicked_(): void {
    if (this.isNumCustomApnsLimitReached_) {
      return;
    }
    this.closeApnMenu_();
    const apnSubpage = castExists(
        this.shadowRoot!.querySelector<ApnSubpageElement>('#apnSubpage'));
    apnSubpage.openApnDetailDialogInCreateMode();
  }

  /**
   * Handles UI requests to discover more APNs.
   */
  private onDiscoverMoreApnsClicked_(): void {
    if (this.isNumCustomApnsLimitReached_) {
      return;
    }
    this.closeApnMenu_();
    const apnSubpage = castExists(
        this.shadowRoot!.querySelector<ApnSubpageElement>('#apnSubpage'));
    apnSubpage.openApnSelectionDialog();
  }

  private shouldDisallowApnModification_(globalPolicy: GlobalPolicy|
                                         undefined): boolean {
    if (!this.isApnRevampAndAllowApnModificationPolicyEnabled_) {
      return false;
    }
    if (!globalPolicy) {
      return false;
    }
    return !globalPolicy.allowApnModification;
  }

  private onShowPasspointDetails_(event: CustomEvent<PasspointSubscription>):
      void {
    this.passpointSubscription_ = event.detail;
    const params = new URLSearchParams();
    params.append('id', this.passpointSubscription_.id);
    Router.getInstance().navigateTo(routes.PASSPOINT_DETAIL, params);
  }

  private getPasspointSubscriptionName_(subscription: PasspointSubscription|
                                        undefined): string {
    if (!subscription) {
      return '';
    }
    if (subscription.friendlyName && subscription.friendlyName !== '') {
      return subscription.friendlyName;
    }
    return subscription.domains[0];
  }

  private showHotspotSpinner_(): boolean {
    if (!this.hotspotInfo) {
      return false;
    }
    return this.hotspotInfo.state === HotspotState.kEnabling ||
        this.hotspotInfo.state === HotspotState.kDisabling;
  }
}

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

customElements.define(
    SettingsInternetPageElement.is, SettingsInternetPageElement);