chromium/ash/webui/common/resources/network/apn_list_item.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 cellular APN in the APN list.
 */

import './network_shared.css.js';
import '//resources/ash/common/cr_elements/cr_action_menu/cr_action_menu.js';

import {assert} from '//resources/ash/common/assert.js';
import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
import {ApnDetailDialogMode, ApnEventData, getApnDisplayName} from '//resources/ash/common/network/cellular_utils.js';
import {MojoInterfaceProviderImpl} from '//resources/ash/common/network/mojo_interface_provider.js';
import {ApnProperties, ApnState, ApnType, CrosNetworkConfigInterface} 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 {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';

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

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

/** @polymer */
class ApnListItem extends ApnListItemBase {
  static get is() {
    return 'apn-list-item';
  }

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

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

      /**@type {!ApnProperties}*/
      apn: {type: Object},

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

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

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

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

      /** The index of this item in its parent list, used for its a11y label. */
      itemIndex: Number,

      /**
       * The total number of elements in this item's parent list, used for its
       * a11y label.
       */
      listSize: Number,

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

      /** @private */
      isDisabled_: {
        reflectToAttribute: true,
        type: Boolean,
        computed: 'computeIsDisabled_(apn)',
      },

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

  constructor() {
    super();
    /** @private {!CrosNetworkConfigInterface} */
    this.networkConfig_ =
        MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
  }

  /**
   * @param {!ApnProperties} apn
   * @private
   */
  getApnDisplayName_(apn) {
    return getApnDisplayName(this.i18n.bind(this), apn);
  }

  /**
   * @return {string}
   * @private
   */
  getSublabel_() {
    if (this.isPortalStateNoInternet_()) {
      return this.i18n('networkListItemConnectedNoConnectivity');
    }
    return this.i18n('OncConnected');
  }

  /**
   * @return {boolean}
   * @private
   */
  isPortalStateNoInternet_() {
    return !!this.portalState && this.portalState === PortalState.kNoInternet;
  }

  /**
   * Opens the three dots menu.
   * @private
   */
  onMenuButtonClicked_(event) {
    /** @type {!CrActionMenuElement} */ (this.$.dotsMenu)
        .showAt(/** @type {!HTMLElement} */ (event.target));
  }

  /** @private */
  closeMenu_() {
    /** @type {!CrActionMenuElement} */ (this.$.dotsMenu).close();
  }

  /**
   * Opens APN Details dialog.
   * @private
   */
  onDetailsClicked_() {
    assert(!!this.apn);
    this.closeMenu_();
    this.dispatchEvent(new CustomEvent('show-apn-detail-dialog', {
      composed: true,
      bubbles: true,
      detail: /** @type {!ApnEventData} */ ({
        apn: this.apn,
        // Only allow editing if the APN is a custom APN.
        mode: this.getDetailDialogMode_(),
      }),
    }));
  }

  /**
   * Returns the mode the APN detail dialog should be if opened.
   * @private
   */
  getDetailDialogMode_() {
    if (!this.apn) {
      return ApnDetailDialogMode.VIEW;
    }

    // Only allow editing if the APN is a user-created custom APN and
    // |AllowAPNModification| is true.
    if (!this.apn.id) {
      return ApnDetailDialogMode.VIEW;
    }
    if (this.isApnRevampAndAllowApnModificationPolicyEnabled_) {
      if (this.shouldDisallowApnModification) {
        return ApnDetailDialogMode.VIEW;
      }
    }
    return ApnDetailDialogMode.EDIT;
  }

  /**
   * Disables the selected APN.
   * @private
   */
  onDisableClicked_() {
    assert(this.guid);
    assert(this.apn);
    this.closeMenu_();
    if (!this.apn.id) {
      console.error('Only custom APNs can be disabled.');
      return;
    }

    if (this.apn.state !== ApnState.kEnabled) {
      console.error('Only an APN that is enabled can be disabled.');
      return;
    }

    if (this.shouldDisallowDisablingRemoving) {
      this.dispatchEvent(new CustomEvent('show-error-toast', {
        bubbles: true,
        composed: true,
        detail: this.i18n('apnWarningPromptForDisableRemove'),
      }));
      return;
    }

    const apn =
        /** @type {!ApnProperties} */ (Object.assign({}, this.apn));
    apn.state = ApnState.kDisabled;
    this.networkConfig_.modifyCustomApn(this.guid, apn);
  }

  /**
   * Enables the selected APN.
   * @private
   */
  onEnableClicked_() {
    assert(this.guid);
    assert(this.apn);
    this.closeMenu_();
    if (!this.apn.id) {
      console.error('Only custom APNs can be enabled.');
      return;
    }

    if (this.apn.state !== ApnState.kDisabled) {
      console.error('Only an APN that is disabled can be enabled.');
      return;
    }

    if (this.shouldDisallowEnabling) {
      this.dispatchEvent(new CustomEvent('show-error-toast', {
        bubbles: true,
        composed: true,
        detail: this.i18n('apnWarningPromptForEnable'),
      }));
      return;
    }

    const apn =
        /** @type {!ApnProperties} */ (Object.assign({}, this.apn));
    apn.state = ApnState.kEnabled;
    this.networkConfig_.modifyCustomApn(this.guid, apn);
  }

  /**
   * Removes the selected APN.
   * @private
   */
  onRemoveClicked_() {
    assert(this.guid);
    assert(this.apn);
    this.closeMenu_();
    if (!this.apn.id) {
      console.error('Only custom APNs can be removed.');
      return;
    }

    if (this.shouldDisallowDisablingRemoving) {
      this.dispatchEvent(new CustomEvent('show-error-toast', {
        bubbles: true,
        composed: true,
        detail: this.i18n('apnWarningPromptForDisableRemove'),
      }));
      return;
    }

    this.networkConfig_.removeCustomApn(
        this.guid, /** @type {string} */ (this.apn.id));
  }

  /**
   * Returns true if disable menu button should be shown.
   * @return {boolean}
   * @private
   */
  shouldShowDisableMenuItem_() {
    return !!this.apn.id && this.apn.state === ApnState.kEnabled;
  }

  /**
   * Returns true if enable menu button should be shown.
   * @return {boolean}
   * @private
   */
  shouldShowEnableMenuItem_() {
    return !!this.apn.id && this.apn.state === ApnState.kDisabled;
  }

  /**
   * Returns true if remove menu button should be shown.
   * @return {boolean}
   * @private
   */
  shouldShowRemoveMenuItem_() {
    return !!this.apn.id;
  }

  /**
   * Returns true if the apn is disabled.
   * @return {boolean}
   * @private
   */
  computeIsDisabled_() {
    return !!this.apn.id && this.apn.state === ApnState.kDisabled;
  }

  /**
   * Returns the label for the "Details" menu item.
   * @return {string}
   * @private
   */
  getDetailsMenuItemLabel_() {
    return this.getDetailDialogMode_() === ApnDetailDialogMode.EDIT ?
        this.i18n('apnMenuEdit') :
        this.i18n('apnMenuDetails');
  }

  /**
   * Returns accessibility label for the item.
   * @return {string}
   * @private
   */
  getAriaLabel_() {
    if (!this.apn) {
      return '';
    }

    let a11yLabel = this.i18n(
        'apnA11yName', this.itemIndex + 1, this.listSize,
        this.getApnDisplayName_(this.apn));

    if (!this.apn.id) {
      a11yLabel += ' ' + this.i18n('apnA11yAutoDetected');
    }

    if (this.isConnected) {
      a11yLabel += ' ' + this.i18n('apnA11yConnected');
    } else if (this.isDisabled_) {
      a11yLabel += ' ' + this.i18n('apnA11yDisabled');
    } else {
      a11yLabel += ' ' + this.i18n('apnA11yEnabled');
    }

    const isDefaultApn =
        this.apn.apnTypes && this.apn.apnTypes.includes(ApnType.kDefault);
    const isAttachApn =
        this.apn.apnTypes && this.apn.apnTypes.includes(ApnType.kAttach);
    if (isDefaultApn && isAttachApn) {
      a11yLabel += ' ' + this.i18n('apnA11yDefaultAndAttachApn');
    } else if (isDefaultApn) {
      a11yLabel += ' ' + this.i18n('apnA11yDefaultApnOnly');
    } else if (isAttachApn) {
      a11yLabel += ' ' + this.i18n('apnA11yAttachApnOnly');
    }

    const userFriendlyName = this.apn.name;
    const name = this.apn.accessPointName;
    if (!!name && !!userFriendlyName && name != userFriendlyName) {
      a11yLabel += ' ' +
          this.i18n('apnA11yUserFriendlyNameIndicator', userFriendlyName, name);
    }
    return a11yLabel;
  }
}

customElements.define(ApnListItem.is, ApnListItem);