chromium/ash/webui/common/resources/network/apn_list.js

// Copyright 2022 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 a list of cellular
 * APNs
 */

import '//resources/ash/common/cr_elements/localized_link/localized_link.js';
import './network_shared.css.js';
import '//resources/polymer/v3_0/iron-list/iron-list.js';
import '//resources/ash/common/network/apn_list_item.js';
import '//resources/ash/common/network/apn_detail_dialog.js';
import '//resources/ash/common/network/apn_selection_dialog.js';
import '//resources/ash/common/cr_elements/icons.html.js';

import {assert} from '//resources/ash/common/assert.js';
import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
import {ApnDetailDialog} from '//resources/ash/common/network/apn_detail_dialog.js';
import {ApnDetailDialogMode, ApnEventData, isAttachApn, isDefaultApn} from '//resources/ash/common/network/cellular_utils.js';
import {ApnProperties, ApnSource, ApnState, ApnType, ManagedCellularProperties} from '//resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {PortalState} from '//resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {afterNextRender, mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

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

/* @type {string} */
const SHILL_INVALID_APN_ERROR = 'invalid-apn';

/**
 * @constructor
 * @extends {PolymerElement}
 * @implements {I18nBehaviorInterface}
 */
const ApnListBase = mixinBehaviors([I18nBehavior], PolymerElement);

/** @polymer */
export class ApnList extends ApnListBase {
  static get is() {
    return 'apn-list';
  }

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

  static get properties() {
    return {
      /** The GUID of the network to display details for. */
      guid: String,

      /**@type {!ManagedCellularProperties}*/
      managedCellularProperties: {
        type: Object,
      },

      errorState: String,

      /** @type {?PortalState} */
      portalState: {
        type: Object,
      },

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

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

      /** @private */
      apns_: {
        type: Object,
        value: [],
        computed: 'computeApns_(managedCellularProperties)',
      },

      /** @private */
      hasEnabledDefaultCustomApn_: {
        type: Boolean,
        computed:
            'computeHasEnabledDefaultCustomApn_(managedCellularProperties)',
      },

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

      /**
       * The mode in which the apn detail dialog is opened.
       * @type {ApnDetailDialogMode}
       * @private
       */
      apnDetailDialogMode_: {
        type: Object,
        value: ApnDetailDialogMode.CREATE,
      },

      /** @private */
      shouldShowApnSelectionDialog_: {
        type: Boolean,
        value: false,
      },
    };
  }

  /**
   * @param {!Event} e
   * @private
   */
  onZeroStateCreateApnLinkClicked_(e) {
    // A place holder href with the value "#" is used to have a compliant link.
    // This prevents the browser from navigating the window to "#"
    e.detail.event.preventDefault();
    e.stopPropagation();
    this.openApnDetailDialogInCreateMode();
  }

  openApnDetailDialogInCreateMode() {
    this.showApnDetailDialog_(ApnDetailDialogMode.CREATE, /* apn= */ undefined);
  }

  openApnSelectionDialog() {
    this.shouldShowApnSelectionDialog_ = true;
  }

  /**
   * @return {boolean}
   * @private
   */
  shouldShowZeroStateContent_() {
    if (!this.managedCellularProperties) {
      return true;
    }

    if (this.managedCellularProperties.connectedApn) {
      return false;
    }

    // Don't display the zero-state text if there's an APN-related error.
    if (this.errorState === SHILL_INVALID_APN_ERROR) {
      return false;
    }

    return !this.getCustomApnList_().length;
  }

  /**
   * @return {boolean}
   * @private
   */
  shouldShowErrorMessage_() {
    // In some instances, there can be an |errorState| and also a connected APN.
    // Don't show the error message as the network is actually connected.
    if (this.managedCellularProperties &&
        this.managedCellularProperties.connectedApn) {
      return false;
    }

    return this.errorState === SHILL_INVALID_APN_ERROR;
  }

  /**
   * @return {string}
   * @private
   */
  getErrorMessage_() {
    if (!this.managedCellularProperties || !this.errorState) {
      return '';
    }

    if (this.getCustomApnList_().some(apn => apn.state === ApnState.kEnabled)) {
      return this.i18n('apnSettingsCustomApnsErrorMessage');
    }

    return this.i18n('apnSettingsDatabaseApnsErrorMessage');
  }

  /**
   * Returns an array with all the APN properties that need to be displayed.
   * TODO(b/162365553): Handle managedCellularProperties.apnList.policyValue
   * when policies are included.
   * @return {Array<!ApnProperties>}
   * @private
   */
  computeApns_() {
    if (!this.managedCellularProperties) {
      return [];
    }

    const {connectedApn} = this.managedCellularProperties;
    const customApnList = this.getCustomApnList_();

    // Move the connected APN to the front if it exists
    if (connectedApn) {
      const customApnsWithoutConnectedApn =
          customApnList.filter(apn => apn.id !== connectedApn.id);
      return [connectedApn, ...customApnsWithoutConnectedApn];
    }
    return customApnList;
  }

  /**
   * Returns true if the APN on this index is connected.
   * @param {number} index index in the APNs array.
   * @return {boolean}
   * @private
   */
  isApnConnected_(index) {
    return !!this.managedCellularProperties &&
        !!this.managedCellularProperties.connectedApn && index === 0;
  }

  /**
   * Returns true if currentApn is the only enabled default APN and there is
   * at least one enabled attach APN.
   * @param {!ApnProperties} currentApn
   * @return {boolean}
   * @private
   */
  shouldDisallowDisablingRemoving_(currentApn) {
    assert(this.managedCellularProperties);
    if (!currentApn.id) {
      return true;
    }

    const customApnList = this.getCustomApnList_();
    if (!customApnList.some(
            apn => isAttachApn(apn) && !isDefaultApn(apn) &&
                apn.state === ApnState.kEnabled)) {
      return false;
    }

    const defaultEnabledApnList = customApnList.filter(
        apn => isDefaultApn(apn) && apn.state === ApnState.kEnabled);

    return defaultEnabledApnList.length === 1 &&
        currentApn.id === defaultEnabledApnList[0].id;
  }

  /**
   * Returns true if there are no enabled default APNs and the current APN has
   * only an attach APN type.
   * @param {!ApnProperties} currentApn
   * @return {boolean}
   * @private
   */
  shouldDisallowEnabling_(currentApn) {
    assert(this.managedCellularProperties);
    if (!currentApn.id) {
      return true;
    }

    if (this.hasEnabledDefaultCustomApn_) {
      return false;
    }

    return isAttachApn(currentApn) && !isDefaultApn(currentApn);
  }

  /**
   * @param {!Event} event
   * @private
   */
  onShowApnDetailDialog_(event) {
    event.stopPropagation();
    if (this.shouldShowApnDetailDialog_) {
      return;
    }
    const eventData = /** @type {!ApnEventData} */ (event.detail);
    this.showApnDetailDialog_(eventData.mode, eventData.apn);
  }

  /**
   * @param {!ApnDetailDialogMode} mode
   * @param {ApnProperties|undefined} apn
   * @private
   */
  showApnDetailDialog_(mode, apn) {
    this.shouldShowApnDetailDialog_ = true;
    this.apnDetailDialogMode_ = mode;
    // Added to ensure dom-if stamping.
    afterNextRender(this, () => {
      const apnDetailDialog = /** @type {ApnDetailDialog} */ (
          this.shadowRoot.querySelector('#apnDetailDialog'));
      assert(!!apnDetailDialog);
      apnDetailDialog.apnProperties = apn;
    });
  }

  /**
   *
   * @param event {!Event}
   * @private
   */
  onApnDetailDialogClose_(event) {
    this.shouldShowApnDetailDialog_ = false;
  }

  /**
   *
   * @param event {!Event}
   * @private
   */
  onApnSelectionDialogClose_(event) {
    this.shouldShowApnSelectionDialog_ = false;
  }

  /**
   * @returns {Array<ApnProperties>}
   * @private
   */
  getCustomApnList_() {
    return this.managedCellularProperties?.customApnList ?? [];
  }

  /**
   * @returns {boolean}
   * @private
   */
  computeHasEnabledDefaultCustomApn_() {
    return this.getCustomApnList_().some(
        (apn) => apn.state === ApnState.kEnabled && isDefaultApn(apn));
  }

  /**
   * @returns {Array<ApnProperties>}
   * @private
   */
  getValidDatabaseApnList_() {
    const databaseApnList =
        this.managedCellularProperties?.apnList?.activeValue ?? [];
    return databaseApnList.filter((apn) => {
      if (apn.source !== ApnSource.kModb) {
        return false;
      }

      // Only APNs that have type default are allowed unless an enabled custom
      // APN of type default already exists. In that case, APNs that are of type
      // attach are also permitted.
      return isDefaultApn(apn) ||
          (this.hasEnabledDefaultCustomApn_ && isAttachApn(apn));
    });
  }
}

customElements.define(ApnList.is, ApnList);