chromium/chrome/browser/resources/ash/settings/internet_page/internet_known_networks_subpage.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-known-networks' is the settings subpage listing the
 * known networks for a type (currently always WiFi).
 */
import 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.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_vars.css.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import './internet_shared.css.js';

import {MojoConnectivityProvider} from 'chrome://resources/ash/common/connectivity/mojo_connectivity_provider.js';
import {PasspointServiceInterface, PasspointSubscription} from 'chrome://resources/ash/common/connectivity/passpoint.mojom-webui.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 {CrActionMenuElement} from 'chrome://resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {ConfigProperties, CrosNetworkConfigInterface, FilterType, NetworkStateProperties, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {DomRepeatEvent, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {assertExists} from '../assert_extras.js';
import {DeepLinkingMixin, DeepLinkingMixinInterface} from '../common/deep_linking_mixin.js';
import {RouteObserverMixin, RouteObserverMixinInterface} from '../common/route_observer_mixin.js';
import {Constructor} from '../common/types.js';
import {recordSettingChange} from '../metrics_recorder.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';
import {Route, routes} from '../router.js';

import {getTemplate} from './internet_known_networks_subpage.html.js';
import {PasspointListenerMixin, PasspointListenerMixinInterface} from './passpoint_listener_mixin.js';

export interface SettingsInternetKnownNetworksPageElement {
  $: {
    dotsMenu: CrActionMenuElement,
    subscriptionDotsMenu: CrActionMenuElement,
  };
}

const SettingsInternetKnownNetworksPageElementBase =
    mixinBehaviors(
        [
          NetworkListenerBehavior,
          CrPolicyNetworkBehaviorMojo,
        ],
        PasspointListenerMixin(
            DeepLinkingMixin(RouteObserverMixin(I18nMixin(PolymerElement))))) as
    Constructor<PolymerElement&I18nMixinInterface&RouteObserverMixinInterface&
                DeepLinkingMixinInterface&NetworkListenerBehaviorInterface&
                CrPolicyNetworkBehaviorMojoInterface&
                PasspointListenerMixinInterface>;

export class SettingsInternetKnownNetworksPageElement extends
    SettingsInternetKnownNetworksPageElementBase {
  static get is() {
    return 'settings-internet-known-networks-subpage' as const;
  }

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

  static get properties() {
    return {
      /**
       * The type of networks to list.
       */
      networkType: {
        type: Number,
        observer: 'networkTypeChanged_',
      },

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

      /**
       * List of all the passpoint subscriptions available.
       */
      passpointSubscriptionsList_: {
        type: Array,
        notify: true,
        value() {
          return [];
        },
      },

      showAddPreferred_: Boolean,

      showRemovePreferred_: Boolean,

      /**
       * We always show 'Forget' since we do not know whether or not to enable
       * it until we fetch the managed properties, and we do not want an empty
       * menu.
       */
      enableForget_: Boolean,

      /**
       * 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.kPreferWifiNetwork,
          Setting.kForgetWifiNetwork,
        ]),
      },
    };
  }

  networkType: NetworkType|undefined;
  private enableForget_: boolean;
  private networkConfig_: CrosNetworkConfigInterface;
  private networkStateList_: OncMojo.NetworkStateProperties[];
  private passpointService_: PasspointServiceInterface;
  private passpointSubscriptionsList_: PasspointSubscription[];
  private pendingSettingId_: Setting|null;
  private selectedGuid_: string;
  private selectedSubscriptionId_: string;
  private showAddPreferred_: boolean;
  private showRemovePreferred_: boolean;

  constructor() {
    super();

    this.selectedGuid_ = '';
    this.selectedSubscriptionId_ = '';

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

    if (this.isPasspointSettingsEnabled) {
      this.passpointService_ =
          MojoConnectivityProvider.getInstance().getPasspointService();
    }
  }

  /**
   * RouteObserverMixin override
   */
  override currentRouteChanged(route: Route): void {
    // Does not apply to this page.
    if (route !== routes.KNOWN_NETWORKS) {
      return;
    }

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

    this.refreshSubscriptions_();
  }

  /** CrosNetworkConfigObserver impl */
  override onNetworkStateListChanged(): void {
    this.refreshNetworks_();
  }

  /** CrosNetworkConfigObserver impl */
  override onNetworkStateChanged(network: NetworkStateProperties): void {
    // Force refresh the networks if we are missing the network state properties
    // or the signal strength is one (WiFi network signal strength is non-zero
    // by convention) since these could indicate the network is not active and
    // would not independently trigger a list update.
    if (!network ||
        (network.type === NetworkType.kWiFi &&
         (!network.typeState.wifi?.signalStrength ||
          network.typeState.wifi?.signalStrength === 1))) {
      this.refreshNetworks_();
    }
  }

  private networkTypeChanged_(): void {
    this.refreshNetworks_();
  }

  /**
   * Requests the list of network states from Chrome. Updates networkStates
   * once the results are returned from Chrome.
   */
  private async refreshNetworks_(): Promise<void> {
    if (this.networkType === undefined) {
      return;
    }
    const filter = {
      filter: FilterType.kConfigured,
      limit: NO_LIMIT,
      networkType: this.networkType,
    };
    const response = await this.networkConfig_.getNetworkStateList(filter);
    this.networkStateList_ = 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 refreshSubscriptions_(): Promise<void> {
    if (!this.isPasspointSettingsEnabled ||
        this.networkType !== NetworkType.kWiFi) {
      this.passpointSubscriptionsList_ = [];
      return;
    }
    const response = await this.passpointService_.listPasspointSubscriptions();
    this.passpointSubscriptionsList_ = response.result;
  }

  private networkIsPreferred_(networkState: OncMojo.NetworkStateProperties):
      boolean {
    // Currently we treat NetworkStateProperties.Priority as a boolean.
    return networkState.priority > 0;
  }

  private networkIsNotPreferred_(networkState: OncMojo.NetworkStateProperties):
      boolean {
    return networkState.priority === 0;
  }

  private havePreferred_(): boolean {
    return this.networkStateList_.find(
               state => this.networkIsPreferred_(state)) !== undefined;
  }

  private haveNotPreferred_(): boolean {
    return this.networkStateList_.find(
               state => this.networkIsNotPreferred_(state)) !== undefined;
  }

  private getNetworkDisplayName_(networkState: OncMojo.NetworkStateProperties):
      string {
    return OncMojo.getNetworkStateDisplayNameUnsafe(networkState);
  }

  private shouldShowPasspointSection_(subscriptionsList:
                                          PasspointSubscription[]): boolean {
    return this.networkType === NetworkType.kWiFi &&
        this.isPasspointSettingsEnabled && subscriptionsList.length > 0;
  }

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

  private getEnterpriseIconAriaLabel_(
      networkState: OncMojo.NetworkStateProperties): string {
    return loadTimeData.getStringF(
        'networkA11yManagedByAdministrator',
        this.getNetworkDisplayName_(networkState));
  }

  private async onMenuButtonClick_(
      event: DomRepeatEvent<OncMojo.NetworkStateProperties>): Promise<void> {
    const button = event.target as HTMLButtonElement;
    const networkState = event.model.item;
    this.selectedGuid_ = networkState.guid;
    // We need to make a round trip to Chrome in order to retrieve the managed
    // properties for the network. The delay is not noticeable (~5ms) and is
    // preferable to initiating a query for every known network at load time.
    const response =
        await this.networkConfig_.getManagedProperties(this.selectedGuid_);
    const properties = response.result;
    if (!properties) {
      console.warn('Properties not found for: ' + this.selectedGuid_);
      return;
    }
    if (properties.priority &&
        this.isNetworkPolicyEnforced(properties.priority)) {
      this.showAddPreferred_ = false;
      this.showRemovePreferred_ = false;
    } else {
      const preferred = this.networkIsPreferred_(networkState);
      this.showAddPreferred_ = !preferred;
      this.showRemovePreferred_ = preferred;
    }
    this.enableForget_ = !this.isPolicySource(networkState.source);
    this.$.dotsMenu.showAt(button);
    event.stopPropagation();
  }

  private getMenuButtonTitle_(networkState: OncMojo.NetworkStateProperties):
      string {
    return loadTimeData.getStringF(
        'knownNetworksMenuButtonTitle',
        this.getNetworkDisplayName_(networkState));
  }

  private async setProperties_(config: ConfigProperties): Promise<void> {
    const response =
        await this.networkConfig_.setProperties(this.selectedGuid_, config);
    if (response.success) {
      recordSettingChange(
          Setting.kPreferWifiNetwork,
          {boolValue: config.priority?.value === 1});
    } else {
      console.warn(
          'Unable to set properties for: ' + this.selectedGuid_ + ': ' +
          JSON.stringify(config));
    }
  }

  private onRemovePreferredClick_(): void {
    assertExists(this.networkType);
    const config = OncMojo.getDefaultConfigProperties(this.networkType);
    config.priority = {value: 0};
    this.setProperties_(config);
    this.$.dotsMenu.close();
  }

  private onAddPreferredClick_(): void {
    assertExists(this.networkType);
    const config = OncMojo.getDefaultConfigProperties(this.networkType);
    config.priority = {value: 1};
    this.setProperties_(config);
    this.$.dotsMenu.close();
  }

  private async onForgetClick_(): Promise<void> {
    this.$.dotsMenu.close();

    const response =
        await this.networkConfig_.forgetNetwork(this.selectedGuid_);
    if (!response.success) {
      console.warn('Forget network failed for: ' + this.selectedGuid_);
      return;
    }

    if (this.networkType === NetworkType.kWiFi) {
      recordSettingChange(Setting.kForgetWifiNetwork);
    } else {
      // TODO(b/282233232) recordSettingChange() for other network types.
    }
  }

  /**
   * Fires a 'show-detail' event with an item containing a |networkStateList_|
   * entry in the event model.
   */
  private fireShowDetails_(
      event: DomRepeatEvent<OncMojo.NetworkStateProperties>): void {
    const networkState = event.model.item;
    const showDetailEvent = new CustomEvent(
        'show-detail', {bubbles: true, composed: true, detail: networkState});
    this.dispatchEvent(showDetailEvent);
    event.stopPropagation();
  }

  private onSubscriptionListItemClick_(
      event: DomRepeatEvent<PasspointSubscription>): void {
    const showPasspointEvent = new CustomEvent(
        'show-passpoint-detail',
        {bubbles: true, composed: true, detail: event.model.item});
    this.dispatchEvent(showPasspointEvent);
    event.stopPropagation();
  }

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

  private onSubscriptionMenuButtonClick_(
      event: DomRepeatEvent<PasspointSubscription>): void {
    const button = event.target as HTMLButtonElement;
    this.selectedSubscriptionId_ = event.model.item.id;
    this.$.subscriptionDotsMenu.showAt(button);
    event.stopPropagation();
  }

  private getSubscriptionMenuButtonTitle_(subscription: PasspointSubscription):
      string {
    return this.i18n(
        'knownNetworksMenuButtonTitle',
        this.getSubscriptionDisplayName_(subscription));
  }

  private async onSubscriptionForgetClick_(): Promise<void> {
    this.$.subscriptionDotsMenu.close();
    const response = await this.passpointService_.deletePasspointSubscription(
        this.selectedSubscriptionId_);
    if (!response.success) {
      console.warn(
          'Forget subscription failed for: ' + this.selectedSubscriptionId_);
    }
    this.selectedSubscriptionId_ = '';
  }

  override async onPasspointSubscriptionAdded(
      subscription: PasspointSubscription): Promise<void> {
    this.push('passpointSubscriptionsList_', subscription);
  }

  override onPasspointSubscriptionRemoved(subscription: PasspointSubscription):
      void {
    const list = this.passpointSubscriptionsList_.filter((sub) => {
      return sub.id !== subscription.id;
    });
    this.passpointSubscriptionsList_ = list;
  }
}

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

customElements.define(
    SettingsInternetKnownNetworksPageElement.is,
    SettingsInternetKnownNetworksPageElement);