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

// 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 Polymer element for displaying and modifying a list of cellular
 * access points.
 */

import '//resources/ash/common/cr_elements/cr_button/cr_button.js';
import '//resources/ash/common/cr_elements/md_select.css.js';
import '//resources/ash/common/cr_elements/policy/cr_tooltip_icon.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import './network_property_list_mojo.js';
import './network_shared.css.js';

import {assert} from '//resources/ash/common/assert.js';
import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
import {OncMojo} from '//resources/ash/common/network/onc_mojo.js';
import {ApnAuthenticationType, ApnIpType, ApnProperties, ApnSource, ApnState, ApnType, ManagedApnProperties, ManagedProperties} from '//resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

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

const kDefaultAccessPointName = 'NONE';
const kOtherAccessPointName = 'Other';

const USE_ATTACH_APN_ON_SAVE_METRIC_NAME =
    'Network.Cellular.Apn.UseAttachApnOnSave';

Polymer({
  _template: getTemplate(),
  is: 'network-apnlist',

  behaviors: [I18nBehavior],

  properties: {
    disabled: {
      type: Boolean,
      value: false,
    },

    /** @type {!ManagedProperties|undefined} */
    managedProperties: {
      type: Object,
      observer: 'managedPropertiesChanged_',
    },

    /**
     * The name property of the selected APN. If a name property is empty, the
     * accessPointName property will be used. We use 'name' so that multiple
     * APNs with the same accessPointName can be supported, so long as they have
     * a unique 'name' property. This is necessary to allow custom  'other'
     * entries (which are always named 'Other') that match an existing
     * accessPointName but provide a different username/password.
     * @private
     */
    selectedApn_: {
      type: String,
      value: '',
    },

    /**
     * Selectable list of APN dictionaries for the UI. Includes an entry
     * corresponding to |otherApn| (see below).
     * @private {!Array<!ApnProperties>}
     */
    apnSelectList_: {
      type: Array,
      value() {
        return [];
      },
    },

    /**
     * The user settable properties for a new ('other') APN. The values are
     * set to default and will be set to the currently active APN if it does
     * not match an existing list entry.
     * @private {!ApnProperties}
     */
    otherApn_: {
      type: Object,
      value() {
        return {
          accessPointName: kDefaultAccessPointName,
          name: kOtherAccessPointName,
          state: ApnState.kEnabled,
          authentication: ApnAuthenticationType.kAutomatic,
          ipType: ApnIpType.kAutomatic,
          apnTypes: [ApnType.kDefault],
          source: ApnSource.kUi,
        };
      },
    },

    /**
     * Array of property names to pass to the Other APN property list.
     * @private {!Array<string>}
     */
    otherApnFields_: {
      type: Array,
      value() {
        return ['accessPointName', 'username', 'password'];
      },
      readOnly: true,
    },

    /**
     * Array of edit types to pass to the Other APN property list.
     * @private
     */
    otherApnEditTypes_: {
      type: Object,
      value() {
        return {
          'accessPointName': 'String',
          'username': 'String',
          'password': 'Password',
        };
      },
      readOnly: true,
    },

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

  /*
   * Returns the select APN SelectElement.
   * @return {?HTMLSelectElement}
   */
  getApnSelect() {
    return /** @type {?HTMLSelectElement} */ (this.$$('#selectApn'));
  },

  /**
   * @param {!ManagedApnProperties} apn
   * @return {!ApnProperties}
   * @private
   */
  getApnFromManaged_(apn) {
    return {
      // authentication and language are ignored in this UI.
      accessPointName: OncMojo.getActiveString(apn.accessPointName),
      localizedName: OncMojo.getActiveString(apn.localizedName),
      name: OncMojo.getActiveString(apn.name),
      password: OncMojo.getActiveString(apn.password),
      username: OncMojo.getActiveString(apn.username),
      authentication: ApnAuthenticationType.kAutomatic,
      // Because this UI is for kApnRevamp=false, these are all default
      // values for fields to pass compilation. None of these fields
      // should be used by CrosNetworkConfig.
      state: ApnState.kEnabled,
      ipType: ApnIpType.kAutomatic,
      apnTypes: [ApnType.kDefault],
      source: ApnSource.kModem,
    };
  },

  /** @private*/
  getActiveApnFromProperties_(managedProperties) {
    const cellular = managedProperties.typeProperties.cellular;
    /** @type {!ApnProperties|undefined} */ let activeApn;
    // We show selectedAPN as the active entry in the select list but it may
    // not correspond to the currently "active" APN which is represented by
    // lastGoodApn.
    if (cellular.selectedApn) {
      activeApn = this.getApnFromManaged_(cellular.selectedApn);
    } else if (cellular.lastGoodApn && cellular.lastGoodApn.accessPointName) {
      activeApn = cellular.lastGoodApn;
    }
    if (activeApn && !activeApn.accessPointName) {
      activeApn = undefined;
    }
    return activeApn;
  },

  /** @private*/
  shouldUpdateSelectList_(oldManagedProperties) {
    if (!oldManagedProperties) {
      return true;
    }

    const newActiveApn =
        this.getActiveApnFromProperties_(this.managedProperties);
    const oldActiveApn = this.getActiveApnFromProperties_(oldManagedProperties);
    if (!OncMojo.apnMatch(newActiveApn, oldActiveApn)) {
      return true;
    }

    const newApnList = this.managedProperties.typeProperties.cellular.apnList;
    const oldApnList = oldManagedProperties.typeProperties.cellular.apnList;
    if (!OncMojo.apnListMatch(
            oldApnList && oldApnList.activeValue,
            newApnList && newApnList.activeValue)) {
      return true;
    }

    const newCustomApnList =
        this.managedProperties.typeProperties.cellular.customApnList;
    const oldCustomApnList =
        oldManagedProperties.typeProperties.cellular.customApnList;
    if (!OncMojo.apnListMatch(oldCustomApnList, newCustomApnList)) {
      return true;
    }

    return false;
  },

  /** @private*/
  managedPropertiesChanged_(managedProperties, oldManagedProperties) {
    if (!this.shouldUpdateSelectList_(oldManagedProperties)) {
      return;
    }
    this.setApnSelectList_(this.getActiveApnFromProperties_(managedProperties));
  },

  /**
   * Sets the list of selectable APNs for the UI. Appends an 'Other' entry
   * (see comments for |otherApn_| above).
   * @param {ApnProperties|undefined} activeApn
   * @private
   */
  setApnSelectList_(activeApn) {
    const apnList = this.generateApnList_();
    if (apnList === undefined || apnList.length === 0) {
      // Show other APN when no APN list property is available.
      this.apnSelectList_ = [this.otherApn_];
      this.set('selectedApn_', kOtherAccessPointName);
      return;
    }
    // Get the list entry for activeApn if it exists. It will have 'name' set.
    let activeApnInList;
    if (activeApn) {
      activeApnInList = apnList.find(a => a.name === activeApn.name);
    }

    const customApnList =
        this.managedProperties.typeProperties.cellular.customApnList;
    let otherApn = this.otherApn_;
    if (customApnList && customApnList.length) {
      // If custom apn list exists, then use it's first entry as otherApn.
      otherApn = customApnList[0];
    } else if (!activeApnInList && activeApn && activeApn.accessPointName) {
      // If the active APN is not in the list, copy it to otherApn.
      otherApn = activeApn;
    }
    this.isAttachApnToggleEnabled_ =
        otherApn.attach === OncMojo.USE_ATTACH_APN_NAME;
    this.otherApn_ = {
      accessPointName: otherApn.accessPointName,
      name: kOtherAccessPointName,
      username: otherApn.username,
      password: otherApn.password,
      authentication: ApnAuthenticationType.kAutomatic,
      // Because this UI is for kApnRevamp=false, these are all default
      // values for fields to pass compilation. None of these fields
      // should be used by CrosNetworkConfig.
      state: ApnState.kEnabled,
      ipType: ApnIpType.kAutomatic,
      apnTypes: [ApnType.kDefault],
      source: ApnSource.kUi,
    };
    apnList.push(this.otherApn_);

    this.apnSelectList_ = apnList;
    const selectedApn =
        activeApnInList ? activeApnInList.name : kOtherAccessPointName;
    assert(selectedApn);
    this.set('selectedApn_', selectedApn);

    // Wait for the dom-repeat to populate the <option> entries then explicitly
    // set the selected value.
    this.async(function() {
      this.$.selectApn.value = this.selectedApn_;
    });
  },

  /**
   * Returns a modified copy of the APN properties or undefined if the
   * property is not set. All entries in the returned copy will have nonempty
   * name and accessPointName properties.
   * @return {!Array<!ApnProperties>|undefined}
   * @private
   */
  generateApnList_() {
    if (!this.managedProperties) {
      return undefined;
    }
    const apnList = this.managedProperties.typeProperties.cellular.apnList;
    if (!apnList) {
      return undefined;
    }
    return apnList.activeValue.filter(apn => !!apn.accessPointName).map(apn => {
      return {
        accessPointName: apn.accessPointName,
        localizedName: apn.localizedName,
        name: apn.name || apn.accessPointName,
        username: apn.username,
        password: apn.password,
      };
    });
  },

  /**
   * Event triggered when the selectApn selection changes.
   * @param {!Event} event
   * @private
   */
  onSelectApnChange_(event) {
    const target = /** @type {!HTMLSelectElement} */ (event.target);
    const name = target.value;
    // When selecting 'Other', don't send a change event unless a valid
    // non-default value has been set for Other.
    if (name === kOtherAccessPointName &&
        (!this.otherApn_.accessPointName ||
         this.otherApn_.accessPointName === kDefaultAccessPointName)) {
      this.selectedApn_ = name;
      return;
    }
    // The change will generate an update which will update selectedApn_ and
    // refresh the UI.
    this.sendApnChange_(name);
  },

  /**
   * Event triggered when any 'Other' APN network property changes.
   * @param {!CustomEvent<!{field: string, value: string}>} event
   * @private
   */
  onOtherApnChange_(event) {
    // TODO(benchan/stevenjb): Move the toUpperCase logic to shill or
    // onc_translator_onc_to_shill.cc.
    const value = (event.detail.field === 'accessPointName') ?
        event.detail.value.toUpperCase() :
        event.detail.value;
    this.set('otherApn_.' + event.detail.field, value);
    // Don't send a change event for 'Other' until the 'Save' button is tapped.
  },

  /**
   * Event triggered when the Other APN 'Save' button is tapped.
   * @private
   */
  onSaveOtherTap_() {
    if (this.sendApnChange_(this.selectedApn_)) {
      chrome.metricsPrivate.recordBoolean(
          USE_ATTACH_APN_ON_SAVE_METRIC_NAME, this.isAttachApnToggleEnabled_);
    }
  },

  /**
   * Attempts to send the apn-change event. Returns true if it succeeds.
   * @param {string} name The APN name property.
   * @return {boolean}
   * @private
   */
  sendApnChange_(name) {
    let apn;
    if (name === kOtherAccessPointName) {
      if (!this.otherApn_.accessPointName ||
          this.otherApn_.accessPointName === kDefaultAccessPointName) {
        // No valid APN set, do nothing.
        return false;
      }
      apn = {
        accessPointName: this.otherApn_.accessPointName,
        username: this.otherApn_.username,
        password: this.otherApn_.password,
        attach: this.isAttachApnToggleEnabled_ ? OncMojo.USE_ATTACH_APN_NAME :
                                                 '',
      };
    } else {
      apn = this.apnSelectList_.find(a => a.name === name);
      if (apn === undefined) {
        // Potential edge case if an update is received before this is invoked.
        console.error('Selected APN not in list');
        return false;
      }
    }
    // Add required field with a default value since it's unused when
    // kApnRevamp=false (b/254549019).
    apn.apnTypes = [ApnType.kDefault];
    this.fire('apn-change', apn);
    return true;
  },

  /**
   * @return {boolean}
   * @private
   */
  isDisabled_() {
    return this.disabled || this.selectedApn_ === '';
  },

  /**
   * @return {boolean}
   * @private
   */
  showOtherApn_() {
    return this.selectedApn_ === kOtherAccessPointName;
  },

  /**
   * @param {!ApnProperties} apn
   * @return {string} The most descriptive name for the access point.
   * @private
   */
  apnDesc_(apn) {
    assert(apn.name);
    return apn.localizedName || apn.name;
  },

  /**
   * @param {ApnProperties} item
   * @return {boolean} Boolean indicating whether |item| is the current selected
   *     apn item.
   * @private
   */
  isApnItemSelected_(item) {
    return item.accessPointName === this.selectedApn_;
  },
});