// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/ash/common/network/cr_policy_network_indicator_mojo.js';
import 'chrome://resources/ash/common/network/network_apnlist.js';
import 'chrome://resources/ash/common/network/network_choose_mobile.js';
import 'chrome://resources/ash/common/network/network_icon.js';
import 'chrome://resources/ash/common/network/network_ip_config.js';
import 'chrome://resources/ash/common/network/network_nameservers.js';
import 'chrome://resources/ash/common/network/network_property_list_mojo.js';
import 'chrome://resources/ash/common/network/network_proxy.js';
import 'chrome://resources/ash/common/network/network_shared.css.js';
import 'chrome://resources/ash/common/network/network_siminfo.js';
import 'chrome://resources/ash/common/cr_elements/cr_expand_button/cr_expand_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_page_host_style.css.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/cr_shared_style.css.js';
import 'chrome://resources/ash/common/cr_elements/cros_color_overrides.css.js';
import 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
import 'chrome://resources/ash/common/network/apn_list.js';
import 'chrome://resources/ash/common/cr_elements/policy/cr_policy_indicator_mixin.js';
import './strings.m.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {ApnList} from 'chrome://resources/ash/common/network/apn_list.js';
import {getApnDisplayName, isActiveSim} from 'chrome://resources/ash/common/network/cellular_utils.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 {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.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 {assert} from 'chrome://resources/js/assert.js';
import {ApnProperties, ConfigProperties, CrosNetworkConfigInterface, GlobalPolicy, IPConfigProperties, ManagedProperties, MAX_NUM_CUSTOM_APNS, ProxySettings, StartConnectResult} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {ConnectionStateType, NetworkType, OncSource, PortalState} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './internet_detail_dialog.html.js';
import {InternetDetailDialogBrowserProxy, InternetDetailDialogBrowserProxyImpl} from './internet_detail_dialog_browser_proxy.js';
/**
* @fileoverview
* 'internet-detail-dialog' is used in the login screen to show a subset of
* internet details and allow configuration of proxy, IP, and nameservers.
*/
const InternetDetailDialogElementBase =
mixinBehaviors(
[NetworkListenerBehavior, CrPolicyNetworkBehaviorMojo],
I18nMixin(PolymerElement)) as {
new (): PolymerElement & I18nMixinInterface &
NetworkListenerBehaviorInterface &
CrPolicyNetworkBehaviorMojoInterface,
};
export class InternetDetailDialogElement extends
InternetDetailDialogElementBase {
static get is() {
return 'internet-detail-dialog' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/** The network GUID to display details for. */
guid: String,
managedProperties_: {
type: Object,
observer: 'managedPropertiesChanged_',
},
deviceState_: {
type: Object,
value: null,
},
/**
* Whether to show technology badge on mobile network icons.
*/
showTechnologyBadge_: {
type: Boolean,
value() {
return loadTimeData.valueExists('showTechnologyBadge') &&
loadTimeData.getBoolean('showTechnologyBadge');
},
},
/**
* Whether network configuration properties sections should be shown. The
* advanced section is not controlled by this property.
*/
showConfigurableSections_: {
type: Boolean,
value: true,
computed: `computeShowConfigurableSections_(deviceState_.*,
managedProperties_.*)`,
},
/**
* When true, all inputs that allow state to be changed (e.g., toggles,
* inputs) are disabled.
*/
disabled_: {
type: Boolean,
value: false,
computed: 'computeDisabled_(deviceState_.*)',
},
globalPolicy_: Object,
apnExpanded_: Boolean,
/**
* Return true if apnRevamp feature flag is enabled.
*/
isApnRevampEnabled_: {
type: Boolean,
value() {
return loadTimeData.valueExists('apnRevamp') &&
loadTimeData.getBoolean('apnRevamp');
},
},
isApnRevampAndAllowApnModificationPolicyEnabled_: {
type: Boolean,
value() {
return loadTimeData.valueExists(
'isApnRevampAndAllowApnModificationPolicyEnabled') &&
loadTimeData.getBoolean(
'isApnRevampAndAllowApnModificationPolicyEnabled');
},
},
/**
* Return true if custom APNs limit is reached.
*/
isNumCustomApnsLimitReached_: {
type: Boolean,
notify: true,
value: false,
computed: 'computeIsNumCustomApnsLimitReached_(managedProperties_)',
},
/**
* The message to be displayed in the error toast when shown.
*/
errorToastMessage_: {
type: String,
value: '',
},
};
}
guid: string;
private managedProperties_: ManagedProperties;
private deviceState_: OncMojo.DeviceStateProperties|null;
private showTechnologyBadge_: boolean;
private showConfigurableSections_: boolean;
private disabled_: boolean;
private globalPolicy_: GlobalPolicy;
private apnExpanded_: boolean;
private isApnRevampEnabled_: boolean;
private isApnRevampAndAllowApnModificationPolicyEnabled_: boolean;
private isNumCustomApnsLimitReached_: boolean;
private errorToastMessage_: string;
private didSetFocus_: boolean = false;
/**
* Set to true to once the initial properties have been received. This
* prevents setProperties from being called when setting default properties.
*/
private propertiesReceived_: boolean = false;
private networkConfig_: CrosNetworkConfigInterface;
private browserProxy_: InternetDetailDialogBrowserProxy;
/** @override */
constructor() {
super();
this.networkConfig_ =
MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
window.CrPolicyStrings = {
controlledSettingPolicy:
loadTimeData.getString('controlledSettingPolicy'),
};
}
override ready() {
super.ready();
this.addEventListener('show-error-toast', (event) => {
this.onShowErrorToast_(event as CustomEvent);
});
}
override connectedCallback() {
super.connectedCallback();
this.browserProxy_ = InternetDetailDialogBrowserProxyImpl.getInstance();
const dialogArgs = this.browserProxy_.getDialogArguments();
ColorChangeUpdater.forDocument().start();
let type;
let name;
if (dialogArgs) {
const args = JSON.parse(dialogArgs);
this.guid = args.guid || '';
type = args.type || 'WiFi';
name = args.name || type;
} else {
// For debugging
const params = new URLSearchParams(document.location.search.substring(1));
this.guid = params.get('guid') || '';
type = params.get('type') || 'WiFi';
name = params.get('name') || type;
}
if (!this.guid) {
console.error('Invalid guid');
this.close_();
}
// Set default managedProperties_ until they are loaded.
this.propertiesReceived_ = false;
this.deviceState_ = null;
this.managedProperties_ = OncMojo.getDefaultManagedProperties(
OncMojo.getNetworkTypeFromString(type), this.guid, name);
this.getNetworkDetails_();
// Fetch global policies.
this.onPoliciesApplied(/*userhash=*/ '');
}
private managedPropertiesChanged_() {
assert(this.managedProperties_);
// Focus the action button once the initial state is set.
if (!this.didSetFocus_ &&
this.showConnectDisconnect_(this.managedProperties_)) {
const button = this.shadowRoot!.querySelector<HTMLElement>(
'#title .action-button:not([hidden])');
if (button) {
button.focus();
this.didSetFocus_ = true;
}
}
}
private close_() {
this.browserProxy_.closeDialog();
}
/** CrosNetworkConfigObserver impl */
override onPoliciesApplied(_userhash: string) {
this.networkConfig_.getGlobalPolicy().then(response => {
this.globalPolicy_ = response.result;
});
}
/** CrosNetworkConfigObserver impl */
override onActiveNetworksChanged(networks: OncMojo.NetworkStateProperties[]) {
if (!this.guid || !this.managedProperties_) {
return;
}
// If the network was or is active, request an update.
if (this.managedProperties_.connectionState !==
ConnectionStateType.kNotConnected ||
networks.find(network => network.guid === this.guid)) {
this.getNetworkDetails_();
}
}
/** CrosNetworkConfigObserver impl */
override onNetworkStateChanged(network: OncMojo.NetworkStateProperties) {
if (!this.guid || !this.managedProperties_) {
return;
}
if (network.guid === this.guid) {
this.getNetworkDetails_();
}
}
/** CrosNetworkConfigObserver impl */
override onDeviceStateListChanged() {
if (!this.guid || !this.managedProperties_) {
return;
}
this.getDeviceState_();
this.getNetworkDetails_();
}
private getNetworkDetails_() {
assert(this.guid);
this.networkConfig_.getManagedProperties(this.guid).then(response => {
if (!response.result) {
// Edge case, may occur when disabling. Close this.
this.close_();
return;
}
this.managedProperties_ = response.result;
this.propertiesReceived_ = true;
if (!this.deviceState_) {
this.getDeviceState_();
}
});
}
private getDeviceState_() {
if (!this.managedProperties_) {
return;
}
const type = this.managedProperties_.type;
this.networkConfig_.getDeviceStateList().then(response => {
const devices = response.result;
this.deviceState_ = devices.find(device => device.type === type) || null;
if (!this.deviceState_) {
// If the device type associated with the current network has been
// removed (e.g., due to unplugging a Cellular dongle), the details
// dialog, if visible, displays controls which are no longer
// functional. If this case occurs, close the dialog.
this.close_();
}
});
}
private getNetworkState_(managedProperties: ManagedProperties):
OncMojo.NetworkStateProperties {
return OncMojo.managedPropertiesToNetworkState(managedProperties);
}
private getDefaultConfigProperties_(): ConfigProperties {
return OncMojo.getDefaultConfigProperties(this.managedProperties_.type);
}
private setMojoNetworkProperties_(config: ConfigProperties) {
if (!this.propertiesReceived_ || !this.guid) {
return;
}
this.networkConfig_.setProperties(this.guid, config).then(response => {
if (!response.success) {
console.error('Unable to set properties: ' + JSON.stringify(config));
// An error typically indicates invalid input; request the properties
// to update any invalid fields.
this.getNetworkDetails_();
}
});
}
private getStateText_(managedProperties: ManagedProperties): string {
if (!managedProperties) {
return '';
}
if (OncMojo.connectionStateIsConnected(managedProperties.connectionState)) {
if (this.isPortalState_(managedProperties.portalState)) {
if (managedProperties.type === NetworkType.kCellular) {
return this.i18n('networkListItemCellularSignIn');
}
return this.i18n('networkListItemSignIn');
}
if (managedProperties.portalState === PortalState.kNoInternet) {
return this.i18n('networkListItemConnectedNoConnectivity');
}
}
return this.i18n(
OncMojo.getConnectionStateString(managedProperties.connectionState));
}
private getNameText_(managedProperties: ManagedProperties): string {
return OncMojo.getNetworkNameUnsafe(managedProperties);
}
/**
* @return True if the network is connected.
*/
private isConnectedState_(managedProperties: (ManagedProperties|undefined)):
boolean {
return !!managedProperties &&
OncMojo.connectionStateIsConnected(managedProperties.connectionState);
}
/**
* @return True if the network is restricted.
*/
private isRestrictedConnectivity_(managedProperties: (ManagedProperties|
undefined)): boolean {
return !!managedProperties &&
OncMojo.isRestrictedConnectivity(managedProperties.portalState);
}
/**
* @return True if the network is connected to have connected color
* for state.
*/
private showConnectedState_(managedProperties: (ManagedProperties|undefined)):
boolean {
return this.isConnectedState_(managedProperties) &&
!this.isRestrictedConnectivity_(managedProperties);
}
/**
* @return True if the network is restricted to have warning color
* for state.
*/
private showRestrictedConnectivity_(managedProperties: (ManagedProperties|
undefined)): boolean {
if (!managedProperties) {
return false;
}
// State must be connected and restricted.
return this.isConnectedState_(managedProperties) &&
this.isRestrictedConnectivity_(managedProperties);
}
private isRemembered_(managedProperties: ManagedProperties): boolean {
return managedProperties.source !== OncSource.kNone;
}
private isRememberedOrConnected_(managedProperties: ManagedProperties):
boolean {
return this.isRemembered_(managedProperties) ||
this.isConnectedState_(managedProperties);
}
private shouldShowApnList_(managedProperties: ManagedProperties): boolean {
return !this.isApnRevampEnabled_ &&
managedProperties.type === NetworkType.kCellular;
}
private shouldShowApnSection_(managedProperties: ManagedProperties): boolean {
return this.isApnRevampEnabled_ &&
managedProperties.type === NetworkType.kCellular;
}
private getApnRowSublabel_(
managedProperties: ManagedProperties, apnExpanded: boolean): string {
if (managedProperties.type !== NetworkType.kCellular ||
!managedProperties.typeProperties.cellular!.connectedApn) {
return '';
}
// Don't show the connected APN if the section has been expanded.
if (apnExpanded) {
return '';
}
return getApnDisplayName(
this.i18n.bind(this),
managedProperties.typeProperties.cellular!.connectedApn);
}
private isApnManaged_(globalPolicy: GlobalPolicy|undefined): boolean {
if (!this.isApnRevampAndAllowApnModificationPolicyEnabled_) {
return false;
}
if (!globalPolicy) {
return false;
}
return !globalPolicy.allowApnModification;
}
private showCellularSim_(managedProperties: ManagedProperties): boolean {
return managedProperties.type === NetworkType.kCellular &&
managedProperties.typeProperties.cellular!.family !== 'CDMA';
}
private showCellularChooseNetwork_(managedProperties: ManagedProperties):
boolean {
return managedProperties.type === NetworkType.kCellular &&
managedProperties.typeProperties.cellular!.supportNetworkScan;
}
private showForget_(managedProperties: ManagedProperties): boolean {
if (!managedProperties || managedProperties.type !== NetworkType.kWiFi) {
return false;
}
return managedProperties.source !== OncSource.kNone &&
!this.isPolicySource(managedProperties.source);
}
private onForgetClicked_() {
this.networkConfig_.forgetNetwork(this.guid).then(response => {
if (!response.success) {
console.error('Forget network failed for: ' + this.guid);
}
// A forgotten network no longer has a valid GUID, close the dialog.
this.close_();
});
}
private showSignin_(managedProperties: (ManagedProperties|undefined)):
boolean {
if (!managedProperties) {
return false;
}
if (OncMojo.connectionStateIsConnected(managedProperties.connectionState) &&
this.isPortalState_(managedProperties.portalState)) {
return true;
}
return false;
}
private disableSignin_(managedProperties: ManagedProperties): boolean {
if (this.disabled_ || !managedProperties) {
return true;
}
if (!OncMojo.connectionStateIsConnected(
managedProperties.connectionState)) {
return true;
}
return !this.isPortalState_(managedProperties.portalState);
}
private onSigninClicked_() {
this.browserProxy_.showPortalSignin(this.guid);
}
private getConnectDisconnectText_(managedProperties: ManagedProperties):
string {
if (this.showConnect_(managedProperties)) {
return this.i18n('networkButtonConnect');
}
return this.i18n('networkButtonDisconnect');
}
private showConnectDisconnect_(managedProperties: (ManagedProperties|
undefined)): boolean {
return this.showConnect_(managedProperties) ||
this.showDisconnect_(managedProperties);
}
private showConnect_(managedProperties: (ManagedProperties|undefined)):
boolean {
if (!managedProperties) {
return false;
}
return managedProperties.connectable &&
managedProperties.type !== NetworkType.kEthernet &&
managedProperties.connectionState === ConnectionStateType.kNotConnected;
}
private showDisconnect_(managedProperties: (ManagedProperties|undefined)):
boolean {
if (!managedProperties) {
return false;
}
return managedProperties.type !== NetworkType.kEthernet &&
managedProperties.connectionState !== ConnectionStateType.kNotConnected;
}
private shouldShowProxyPolicyIndicator_(managedProperties: ManagedProperties):
boolean {
if (!managedProperties.proxySettings) {
return false;
}
return this.isNetworkPolicyEnforced(managedProperties.proxySettings.type);
}
private enableConnectDisconnect_(managedProperties: ManagedProperties):
boolean {
if (this.disabled_) {
return false;
}
if (!this.showConnectDisconnect_(managedProperties)) {
return false;
}
if (this.showConnect_(managedProperties)) {
return this.enableConnect_(managedProperties);
}
return true;
}
/**
* @return Whether or not to enable the network connect button.
*/
private enableConnect_(managedProperties: ManagedProperties): boolean {
return this.showConnect_(managedProperties);
}
private onConnectDisconnectClick_() {
if (!this.managedProperties_) {
return;
}
if (!this.showConnect_(this.managedProperties_)) {
this.networkConfig_.startDisconnect(this.guid);
return;
}
const guid = this.managedProperties_.guid;
this.networkConfig_.startConnect(this.guid).then(response => {
switch (response.result) {
case StartConnectResult.kSuccess:
break;
case StartConnectResult.kInvalidState:
case StartConnectResult.kCanceled:
// Ignore failures due to in-progress or cancelled connects.
break;
case StartConnectResult.kInvalidGuid:
case StartConnectResult.kNotConfigured:
case StartConnectResult.kBlocked:
case StartConnectResult.kUnknown:
console.error(
'Unexpected startConnect error for: ' + guid + ' Result: ' +
response.result.toString() + ' Message: ' + response.message);
break;
}
});
}
private onApnChange_(event: CustomEvent<ApnProperties>) {
if (!this.propertiesReceived_) {
return;
}
const config = this.getDefaultConfigProperties_();
const apn = event.detail;
config.typeConfig.cellular = {
apn: apn,
roaming: undefined,
textMessageAllowState: undefined,
};
this.setMojoNetworkProperties_(config);
}
/**
* Event triggered when the IP Config or NameServers element changes.
* @param event The network-ip-config or network-nameservers change event.
*/
private onIpConfigChange_(
event: CustomEvent<
{field: string, value: string|IPConfigProperties|string[]}>) {
if (!this.managedProperties_) {
return;
}
const config = OncMojo.getUpdatedIPConfigProperties(
this.managedProperties_, event.detail.field, event.detail.value);
if (config) {
this.setMojoNetworkProperties_(config);
}
}
/**
* Event triggered when the Proxy configuration element changes.
*/
private onProxyChange_(event: CustomEvent<ProxySettings>) {
if (!this.propertiesReceived_) {
return;
}
const config = this.getDefaultConfigProperties_();
config.proxySettings = event.detail;
this.setMojoNetworkProperties_(config);
}
private hasVisibleFields_(fields: string[]): boolean {
return fields.some((field) => {
const key = OncMojo.getManagedPropertyKey(field);
const value = this.get(key, this.managedProperties_);
return value !== undefined && value !== '';
});
}
private hasInfoFields_(): boolean {
return this.hasVisibleFields_(this.getInfoFields_());
}
/**
* @return The fields to display in the info section.
*/
private getInfoFields_(): string[] {
const fields: string[] = [];
const type = this.managedProperties_.type;
if (type === NetworkType.kCellular) {
fields.push(
'cellular.activationState', 'cellular.servingOperator.name',
'cellular.networkTechnology');
}
if (OncMojo.isRestrictedConnectivity(this.managedProperties_.portalState)) {
fields.push('portalState');
}
// Two separate checks for type === kCellular because the order of the array
// dictates the order the fields appear on the UI. We want portalState to
// show after the earlier Cellular fields but before these later fields.
if (type === NetworkType.kCellular) {
fields.push(
'cellular.homeProvider.name', 'cellular.homeProvider.country',
'cellular.firmwareRevision', 'cellular.hardwareRevision',
'cellular.esn', 'cellular.iccid', 'cellular.imei', 'cellular.meid',
'cellular.min');
}
return fields;
}
private computeShowConfigurableSections_(): boolean {
if (!this.managedProperties_ || !this.deviceState_) {
return true;
}
if (this.managedProperties_.type !== NetworkType.kCellular) {
return true;
}
const networkState =
OncMojo.managedPropertiesToNetworkState(this.managedProperties_);
assert(networkState);
return isActiveSim(networkState, this.deviceState_);
}
private computeDisabled_(): boolean {
if (!this.deviceState_ ||
this.deviceState_.type !== NetworkType.kCellular) {
return false;
}
// If this is a cellular device and inhibited, state cannot be changed, so
// the dialog's inputs should be disabled.
return OncMojo.deviceIsInhibited(this.deviceState_);
}
private isPortalState_(portalState: PortalState): boolean {
return portalState === PortalState.kPortal ||
portalState === PortalState.kPortalSuspected;
}
/**
* Handles UI requests to create new custom APN.
*/
private onCreateCustomApnClicked_() {
if (this.isNumCustomApnsLimitReached_) {
return;
}
assert(!!this.guid);
const apnList = this.shadowRoot!.querySelector<ApnList>('#apnList');
assert(apnList);
apnList.openApnDetailDialogInCreateMode();
}
/**
* Handles UI requests to discover known APNs.
*/
private onDiscoverMoreApnsClicked_() {
if (this.isNumCustomApnsLimitReached_) {
return;
}
assert(!!this.guid);
const apnList = this.shadowRoot!.querySelector<ApnList>('#apnList');
assert(apnList);
apnList.openApnSelectionDialog();
}
private computeIsNumCustomApnsLimitReached_(): boolean {
if (!this.managedProperties_ ||
this.managedProperties_.type !== NetworkType.kCellular ||
!this.managedProperties_.typeProperties ||
!this.managedProperties_.typeProperties.cellular) {
return false;
}
const customApnList =
this.managedProperties_.typeProperties.cellular.customApnList;
return !!customApnList && customApnList.length >= MAX_NUM_CUSTOM_APNS;
}
private shouldDisableApnButtons_(): boolean {
if (!this.isApnRevampEnabled_) {
return true;
}
if (!this.isApnRevampAndAllowApnModificationPolicyEnabled_) {
return this.isNumCustomApnsLimitReached_;
}
return this.isNumCustomApnsLimitReached_ ||
this.isApnManaged_(this.globalPolicy_);
}
private onShowErrorToast_(event: CustomEvent<string>) {
if (!this.isApnRevampEnabled_) {
return;
}
this.errorToastMessage_ = event.detail;
const errorToast =
this.shadowRoot!.querySelector<CrToastElement>('#errorToast');
assert(errorToast);
errorToast.show();
}
private shouldShowMacAddress_(macAddress: string): boolean {
return !!macAddress && macAddress.length > 0 &&
macAddress !== '00:00:00:00:00:00';
}
}
declare global {
interface HTMLElementTagNameMap {
[InternetDetailDialogElement.is]: InternetDetailDialogElement;
}
}
customElements.define(
InternetDetailDialogElement.is, InternetDetailDialogElement);