diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 15:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 15:07:22 +0000 |
commit | f9d480cfe50ca1d7a0f0b5a2b8bb9932962bfbe7 (patch) | |
tree | ce9e8db2d4e8799780fa72ae8f1953039373e2ee /js/ui/status/network.js | |
parent | Initial commit. (diff) | |
download | gnome-shell-upstream.tar.xz gnome-shell-upstream.zip |
Adding upstream version 3.38.6.upstream/3.38.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/ui/status/network.js')
-rw-r--r-- | js/ui/status/network.js | 2101 |
1 files changed, 2101 insertions, 0 deletions
diff --git a/js/ui/status/network.js b/js/ui/status/network.js new file mode 100644 index 0000000..377f44e --- /dev/null +++ b/js/ui/status/network.js @@ -0,0 +1,2101 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported NMApplet */ +const { Clutter, Gio, GLib, GObject, NM, St } = imports.gi; +const Signals = imports.signals; + +const Animation = imports.ui.animation; +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const MessageTray = imports.ui.messageTray; +const ModalDialog = imports.ui.modalDialog; +const ModemManager = imports.misc.modemManager; +const Rfkill = imports.ui.status.rfkill; +const Util = imports.misc.util; + +const { loadInterfaceXML } = imports.misc.fileUtils; + +Gio._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish'); +Gio._promisify(NM.Client, 'new_async', 'new_finish'); +Gio._promisify(NM.Client.prototype, + 'check_connectivity_async', 'check_connectivity_finish'); + +const NMConnectionCategory = { + INVALID: 'invalid', + WIRED: 'wired', + WIRELESS: 'wireless', + WWAN: 'wwan', + VPN: 'vpn', +}; + +const NMAccessPointSecurity = { + NONE: 1, + WEP: 2, + WPA_PSK: 3, + WPA2_PSK: 4, + WPA_ENT: 5, + WPA2_ENT: 6, +}; + +var MAX_DEVICE_ITEMS = 4; + +// small optimization, to avoid using [] all the time +const NM80211Mode = NM['80211Mode']; +const NM80211ApFlags = NM['80211ApFlags']; +const NM80211ApSecurityFlags = NM['80211ApSecurityFlags']; + +var PortalHelperResult = { + CANCELLED: 0, + COMPLETED: 1, + RECHECK: 2, +}; + +const PortalHelperIface = loadInterfaceXML('org.gnome.Shell.PortalHelper'); +const PortalHelperProxy = Gio.DBusProxy.makeProxyWrapper(PortalHelperIface); + +function signalToIcon(value) { + if (value > 80) + return 'excellent'; + if (value > 55) + return 'good'; + if (value > 30) + return 'ok'; + if (value > 5) + return 'weak'; + return 'none'; +} + +function ssidToLabel(ssid) { + let label = NM.utils_ssid_to_utf8(ssid.get_data()); + if (!label) + label = _("<unknown>"); + return label; +} + +function ensureActiveConnectionProps(active) { + if (!active._primaryDevice) { + let devices = active.get_devices(); + if (devices.length > 0) { + // This list is guaranteed to have at most one device in it. + let device = devices[0]._delegate; + active._primaryDevice = device; + } + } +} + +function launchSettingsPanel(panel, ...args) { + const param = new GLib.Variant('(sav)', + [panel, args.map(s => new GLib.Variant('s', s))]); + const platformData = { + 'desktop-startup-id': new GLib.Variant('s', + '_TIME%s'.format(global.get_current_time())), + }; + try { + Gio.DBus.session.call( + 'org.gnome.ControlCenter', + '/org/gnome/ControlCenter', + 'org.freedesktop.Application', + 'ActivateAction', + new GLib.Variant('(sava{sv})', + ['launch-panel', [param], platformData]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + } catch (e) { + log('Failed to launch Settings panel: %s'.format(e.message)); + } +} + +var NMConnectionItem = class { + constructor(section, connection) { + this._section = section; + this._connection = connection; + this._activeConnection = null; + this._activeConnectionChangedId = 0; + + this._buildUI(); + this._sync(); + } + + _buildUI() { + this.labelItem = new PopupMenu.PopupMenuItem(''); + this.labelItem.connect('activate', this._toggle.bind(this)); + + this.radioItem = new PopupMenu.PopupMenuItem(this._connection.get_id(), false); + this.radioItem.connect('activate', this._activate.bind(this)); + } + + destroy() { + this.labelItem.destroy(); + this.radioItem.destroy(); + } + + updateForConnection(connection) { + // connection should always be the same object + // (and object path) as this._connection, but + // this can be false if NetworkManager was restarted + // and picked up connections in a different order + // Just to be safe, we set it here again + + this._connection = connection; + this.radioItem.label.text = connection.get_id(); + this._sync(); + this.emit('name-changed'); + } + + getName() { + return this._connection.get_id(); + } + + isActive() { + if (this._activeConnection == null) + return false; + + return this._activeConnection.state <= NM.ActiveConnectionState.ACTIVATED; + } + + _sync() { + let isActive = this.isActive(); + this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel(); + this.radioItem.setOrnament(isActive ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE); + this.emit('icon-changed'); + } + + _toggle() { + if (this._activeConnection == null) + this._section.activateConnection(this._connection); + else + this._section.deactivateConnection(this._activeConnection); + + this._sync(); + } + + _activate() { + if (this._activeConnection == null) + this._section.activateConnection(this._connection); + + this._sync(); + } + + _connectionStateChanged(_ac, _newstate, _reason) { + this._sync(); + } + + setActiveConnection(activeConnection) { + if (this._activeConnectionChangedId > 0) { + this._activeConnection.disconnect(this._activeConnectionChangedId); + this._activeConnectionChangedId = 0; + } + + this._activeConnection = activeConnection; + + if (this._activeConnection) { + this._activeConnectionChangedId = this._activeConnection.connect('notify::state', + this._connectionStateChanged.bind(this)); + } + + this._sync(); + } +}; +Signals.addSignalMethods(NMConnectionItem.prototype); + +var NMConnectionSection = class NMConnectionSection { + constructor(client) { + if (this.constructor === NMConnectionSection) + throw new TypeError('Cannot instantiate abstract type %s'.format(this.constructor.name)); + + this._client = client; + + this._connectionItems = new Map(); + this._connections = []; + + this._labelSection = new PopupMenu.PopupMenuSection(); + this._radioSection = new PopupMenu.PopupMenuSection(); + + this.item = new PopupMenu.PopupSubMenuMenuItem('', true); + this.item.menu.addMenuItem(this._labelSection); + this.item.menu.addMenuItem(this._radioSection); + + this._notifyConnectivityId = this._client.connect('notify::connectivity', this._iconChanged.bind(this)); + } + + destroy() { + if (this._notifyConnectivityId != 0) { + this._client.disconnect(this._notifyConnectivityId); + this._notifyConnectivityId = 0; + } + + this.item.destroy(); + } + + _iconChanged() { + this._sync(); + this.emit('icon-changed'); + } + + _sync() { + let nItems = this._connectionItems.size; + + this._radioSection.actor.visible = nItems > 1; + this._labelSection.actor.visible = nItems == 1; + + this.item.label.text = this._getStatus(); + this.item.icon.icon_name = this._getMenuIcon(); + } + + _getMenuIcon() { + return this.getIndicatorIcon(); + } + + getConnectLabel() { + return _("Connect"); + } + + _connectionValid(_connection) { + return true; + } + + _connectionSortFunction(one, two) { + return GLib.utf8_collate(one.get_id(), two.get_id()); + } + + _makeConnectionItem(connection) { + return new NMConnectionItem(this, connection); + } + + checkConnection(connection) { + if (!this._connectionValid(connection)) + return; + + // This function is called every time the connection is added or updated. + // In the usual case, we already added this connection and UUID + // didn't change. So we need to check if we already have an item, + // and update it for properties in the connection that changed + // (the only one we care about is the name) + // But it's also possible we didn't know about this connection + // (eg, during coldplug, or because it was updated and suddenly + // it's valid for this device), in which case we add a new item. + + let item = this._connectionItems.get(connection.get_uuid()); + if (item) + this._updateForConnection(item, connection); + else + this._addConnection(connection); + } + + _updateForConnection(item, connection) { + let pos = this._connections.indexOf(connection); + + this._connections.splice(pos, 1); + pos = Util.insertSorted(this._connections, connection, this._connectionSortFunction.bind(this)); + this._labelSection.moveMenuItem(item.labelItem, pos); + this._radioSection.moveMenuItem(item.radioItem, pos); + + item.updateForConnection(connection); + } + + _addConnection(connection) { + let item = this._makeConnectionItem(connection); + if (!item) + return; + + item.connect('icon-changed', () => this._iconChanged()); + item.connect('activation-failed', (o, reason) => { + this.emit('activation-failed', reason); + }); + item.connect('name-changed', this._sync.bind(this)); + + let pos = Util.insertSorted(this._connections, connection, this._connectionSortFunction.bind(this)); + this._labelSection.addMenuItem(item.labelItem, pos); + this._radioSection.addMenuItem(item.radioItem, pos); + this._connectionItems.set(connection.get_uuid(), item); + this._sync(); + } + + removeConnection(connection) { + let uuid = connection.get_uuid(); + let item = this._connectionItems.get(uuid); + if (item == undefined) + return; + + item.destroy(); + this._connectionItems.delete(uuid); + + let pos = this._connections.indexOf(connection); + this._connections.splice(pos, 1); + + this._sync(); + } +}; +Signals.addSignalMethods(NMConnectionSection.prototype); + +var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection { + constructor(client, device) { + super(client); + + if (this.constructor === NMConnectionDevice) + throw new TypeError('Cannot instantiate abstract type %s'.format(this.constructor.name)); + + this._device = device; + this._description = ''; + + this._autoConnectItem = this.item.menu.addAction(_("Connect"), this._autoConnect.bind(this)); + this._deactivateItem = this._radioSection.addAction(_("Turn Off"), this.deactivateConnection.bind(this)); + + this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this)); + this._activeConnectionChangedId = this._device.connect('notify::active-connection', this._activeConnectionChanged.bind(this)); + } + + _canReachInternet() { + if (this._client.primary_connection != this._device.active_connection) + return true; + + return this._client.connectivity == NM.ConnectivityState.FULL; + } + + _autoConnect() { + let connection = new NM.SimpleConnection(); + this._client.add_and_activate_connection_async(connection, this._device, null, null, null); + } + + destroy() { + if (this._stateChangedId) { + GObject.signal_handler_disconnect(this._device, this._stateChangedId); + this._stateChangedId = 0; + } + if (this._activeConnectionChangedId) { + GObject.signal_handler_disconnect(this._device, this._activeConnectionChangedId); + this._activeConnectionChangedId = 0; + } + + super.destroy(); + } + + _activeConnectionChanged() { + if (this._activeConnection) { + let item = this._connectionItems.get(this._activeConnection.connection.get_uuid()); + item.setActiveConnection(null); + this._activeConnection = null; + } + + this._sync(); + } + + _deviceStateChanged(device, newstate, oldstate, reason) { + if (newstate == oldstate) { + log('device emitted state-changed without actually changing state'); + return; + } + + /* Emit a notification if activation fails, but don't do it + if the reason is no secrets, as that indicates the user + cancelled the agent dialog */ + if (newstate == NM.DeviceState.FAILED && + reason != NM.DeviceStateReason.NO_SECRETS) + this.emit('activation-failed', reason); + + this._sync(); + } + + _connectionValid(connection) { + return this._device.connection_valid(connection); + } + + activateConnection(connection) { + this._client.activate_connection_async(connection, this._device, null, null, null); + } + + deactivateConnection(_activeConnection) { + this._device.disconnect(null); + } + + setDeviceDescription(desc) { + this._description = desc; + this._sync(); + } + + _getDescription() { + return this._description; + } + + _sync() { + let nItems = this._connectionItems.size; + this._autoConnectItem.visible = nItems == 0; + this._deactivateItem.visible = this._device.state > NM.DeviceState.DISCONNECTED; + + if (this._activeConnection == null) { + let activeConnection = this._device.active_connection; + if (activeConnection && activeConnection.connection) { + let item = this._connectionItems.get(activeConnection.connection.get_uuid()); + if (item) { + this._activeConnection = activeConnection; + ensureActiveConnectionProps(this._activeConnection); + item.setActiveConnection(this._activeConnection); + } + } + } + + super._sync(); + } + + _getStatus() { + if (!this._device) + return ''; + + switch (this._device.state) { + case NM.DeviceState.DISCONNECTED: + /* Translators: %s is a network identifier */ + return _("%s Off").format(this._getDescription()); + case NM.DeviceState.ACTIVATED: + /* Translators: %s is a network identifier */ + return _("%s Connected").format(this._getDescription()); + case NM.DeviceState.UNMANAGED: + /* Translators: this is for network devices that are physically present but are not + under NetworkManager's control (and thus cannot be used in the menu); + %s is a network identifier */ + return _("%s Unmanaged").format(this._getDescription()); + case NM.DeviceState.DEACTIVATING: + /* Translators: %s is a network identifier */ + return _("%s Disconnecting").format(this._getDescription()); + case NM.DeviceState.PREPARE: + case NM.DeviceState.CONFIG: + case NM.DeviceState.IP_CONFIG: + case NM.DeviceState.IP_CHECK: + case NM.DeviceState.SECONDARIES: + /* Translators: %s is a network identifier */ + return _("%s Connecting").format(this._getDescription()); + case NM.DeviceState.NEED_AUTH: + /* Translators: this is for network connections that require some kind of key or password; %s is a network identifier */ + return _("%s Requires Authentication").format(this._getDescription()); + case NM.DeviceState.UNAVAILABLE: + // This state is actually a compound of various states (generically unavailable, + // firmware missing), that are exposed by different properties (whose state may + // or may not updated when we receive state-changed). + if (this._device.firmware_missing) { + /* Translators: this is for devices that require some kind of firmware or kernel + module, which is missing; %s is a network identifier */ + return _("Firmware Missing For %s").format(this._getDescription()); + } + /* Translators: this is for a network device that cannot be activated (for example it + is disabled by rfkill, or it has no coverage; %s is a network identifier */ + return _("%s Unavailable").format(this._getDescription()); + case NM.DeviceState.FAILED: + /* Translators: %s is a network identifier */ + return _("%s Connection Failed").format(this._getDescription()); + default: + log('Device state invalid, is %d'.format(this._device.state)); + return 'invalid'; + } + } +}; + +var NMDeviceWired = class extends NMConnectionDevice { + constructor(client, device) { + super(client, device); + + this.item.menu.addSettingsAction(_("Wired Settings"), 'gnome-network-panel.desktop'); + } + + get category() { + return NMConnectionCategory.WIRED; + } + + _hasCarrier() { + if (this._device instanceof NM.DeviceEthernet) + return this._device.carrier; + else + return true; + } + + _sync() { + this.item.visible = this._hasCarrier(); + super._sync(); + } + + getIndicatorIcon() { + if (this._device.active_connection) { + let state = this._device.active_connection.state; + + if (state == NM.ActiveConnectionState.ACTIVATING) { + return 'network-wired-acquiring-symbolic'; + } else if (state == NM.ActiveConnectionState.ACTIVATED) { + if (this._canReachInternet()) + return 'network-wired-symbolic'; + else + return 'network-wired-no-route-symbolic'; + } else { + return 'network-wired-disconnected-symbolic'; + } + } else { + return 'network-wired-disconnected-symbolic'; + } + } +}; + +var NMDeviceModem = class extends NMConnectionDevice { + constructor(client, device) { + super(client, device); + + this.item.menu.addSettingsAction(_("Mobile Broadband Settings"), 'gnome-network-panel.desktop'); + + this._mobileDevice = null; + + let capabilities = device.current_capabilities; + if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0) + this._mobileDevice = new ModemManager.BroadbandModem(device.udi, capabilities); + else if (capabilities & NM.DeviceModemCapabilities.GSM_UMTS) + this._mobileDevice = new ModemManager.ModemGsm(device.udi); + else if (capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) + this._mobileDevice = new ModemManager.ModemCdma(device.udi); + else if (capabilities & NM.DeviceModemCapabilities.LTE) + this._mobileDevice = new ModemManager.ModemGsm(device.udi); + + if (this._mobileDevice) { + this._operatorNameId = this._mobileDevice.connect('notify::operator-name', this._sync.bind(this)); + this._signalQualityId = this._mobileDevice.connect('notify::signal-quality', () => { + this._iconChanged(); + }); + } + } + + get category() { + return NMConnectionCategory.WWAN; + } + + _autoConnect() { + launchSettingsPanel('network', 'connect-3g', this._device.get_path()); + } + + destroy() { + if (this._operatorNameId) { + this._mobileDevice.disconnect(this._operatorNameId); + this._operatorNameId = 0; + } + if (this._signalQualityId) { + this._mobileDevice.disconnect(this._signalQualityId); + this._signalQualityId = 0; + } + + super.destroy(); + } + + _getStatus() { + if (!this._client.wwan_hardware_enabled) + /* Translators: %s is a network identifier */ + return _("%s Hardware Disabled").format(this._getDescription()); + else if (!this._client.wwan_enabled) + /* Translators: this is for a network device that cannot be activated + because it's disabled by rfkill (airplane mode); %s is a network identifier */ + return _("%s Disabled").format(this._getDescription()); + else if (this._device.state == NM.DeviceState.ACTIVATED && + this._mobileDevice && this._mobileDevice.operator_name) + return this._mobileDevice.operator_name; + else + return super._getStatus(); + } + + getIndicatorIcon() { + if (this._device.active_connection) { + if (this._device.active_connection.state == NM.ActiveConnectionState.ACTIVATING) + return 'network-cellular-acquiring-symbolic'; + + return this._getSignalIcon(); + } else { + return 'network-cellular-signal-none-symbolic'; + } + } + + _getSignalIcon() { + return 'network-cellular-signal-%s-symbolic'.format( + signalToIcon(this._mobileDevice.signal_quality)); + } +}; + +var NMDeviceBluetooth = class extends NMConnectionDevice { + constructor(client, device) { + super(client, device); + + this.item.menu.addSettingsAction(_("Bluetooth Settings"), 'gnome-network-panel.desktop'); + } + + get category() { + return NMConnectionCategory.WWAN; + } + + _getDescription() { + return this._device.name; + } + + getConnectLabel() { + return _("Connect to Internet"); + } + + getIndicatorIcon() { + if (this._device.active_connection) { + let state = this._device.active_connection.state; + if (state == NM.ActiveConnectionState.ACTIVATING) + return 'network-cellular-acquiring-symbolic'; + else if (state == NM.ActiveConnectionState.ACTIVATED) + return 'network-cellular-connected-symbolic'; + else + return 'network-cellular-signal-none-symbolic'; + } else { + return 'network-cellular-signal-none-symbolic'; + } + } +}; + +var NMWirelessDialogItem = GObject.registerClass({ + Signals: { + 'selected': {}, + }, +}, class NMWirelessDialogItem extends St.BoxLayout { + _init(network) { + this._network = network; + this._ap = network.accessPoints[0]; + + super._init({ style_class: 'nm-dialog-item', + can_focus: true, + reactive: true }); + + let action = new Clutter.ClickAction(); + action.connect('clicked', () => this.grab_key_focus()); + this.add_action(action); + + let title = ssidToLabel(this._ap.get_ssid()); + this._label = new St.Label({ + text: title, + x_expand: true, + }); + + this.label_actor = this._label; + this.add_child(this._label); + + this._selectedIcon = new St.Icon({ style_class: 'nm-dialog-icon', + icon_name: 'object-select-symbolic' }); + this.add(this._selectedIcon); + + this._icons = new St.BoxLayout({ + style_class: 'nm-dialog-icons', + x_align: Clutter.ActorAlign.END, + }); + this.add_child(this._icons); + + this._secureIcon = new St.Icon({ style_class: 'nm-dialog-icon' }); + if (this._ap._secType != NMAccessPointSecurity.NONE) + this._secureIcon.icon_name = 'network-wireless-encrypted-symbolic'; + this._icons.add_actor(this._secureIcon); + + this._signalIcon = new St.Icon({ style_class: 'nm-dialog-icon' }); + this._icons.add_actor(this._signalIcon); + + this._sync(); + } + + vfunc_key_focus_in() { + this.emit('selected'); + } + + _sync() { + this._signalIcon.icon_name = this._getSignalIcon(); + } + + updateBestAP(ap) { + this._ap = ap; + this._sync(); + } + + setActive(isActive) { + this._selectedIcon.opacity = isActive ? 255 : 0; + } + + _getSignalIcon() { + if (this._ap.mode == NM80211Mode.ADHOC) { + return 'network-workgroup-symbolic'; + } else { + return 'network-wireless-signal-%s-symbolic'.format( + signalToIcon(this._ap.strength)); + } + } +}); + +var NMWirelessDialog = GObject.registerClass( +class NMWirelessDialog extends ModalDialog.ModalDialog { + _init(client, device) { + super._init({ styleClass: 'nm-dialog' }); + + this._client = client; + this._device = device; + + this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled', + this._syncView.bind(this)); + + this._rfkill = Rfkill.getRfkillManager(); + this._airplaneModeChangedId = this._rfkill.connect('airplane-mode-changed', + this._syncView.bind(this)); + + this._networks = []; + this._buildLayout(); + + let connections = client.get_connections(); + this._connections = connections.filter( + connection => device.connection_valid(connection)); + + this._apAddedId = device.connect('access-point-added', this._accessPointAdded.bind(this)); + this._apRemovedId = device.connect('access-point-removed', this._accessPointRemoved.bind(this)); + this._activeApChangedId = device.connect('notify::active-access-point', this._activeApChanged.bind(this)); + + // accessPointAdded will also create dialog items + let accessPoints = device.get_access_points() || []; + accessPoints.forEach(ap => { + this._accessPointAdded(this._device, ap); + }); + + this._selectedNetwork = null; + this._activeApChanged(); + this._updateSensitivity(); + this._syncView(); + + this._scanTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 15, this._onScanTimeout.bind(this)); + GLib.Source.set_name_by_id(this._scanTimeoutId, '[gnome-shell] this._onScanTimeout'); + this._onScanTimeout(); + + let id = Main.sessionMode.connect('updated', () => { + if (Main.sessionMode.allowSettings) + return; + + Main.sessionMode.disconnect(id); + this.close(); + }); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _onDestroy() { + if (this._apAddedId) { + GObject.Object.prototype.disconnect.call(this._device, this._apAddedId); + this._apAddedId = 0; + } + if (this._apRemovedId) { + GObject.Object.prototype.disconnect.call(this._device, this._apRemovedId); + this._apRemovedId = 0; + } + if (this._activeApChangedId) { + GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId); + this._activeApChangedId = 0; + } + if (this._wirelessEnabledChangedId) { + this._client.disconnect(this._wirelessEnabledChangedId); + this._wirelessEnabledChangedId = 0; + } + if (this._airplaneModeChangedId) { + this._rfkill.disconnect(this._airplaneModeChangedId); + this._airplaneModeChangedId = 0; + } + + if (this._scanTimeoutId) { + GLib.source_remove(this._scanTimeoutId); + this._scanTimeoutId = 0; + } + } + + _onScanTimeout() { + this._device.request_scan_async(null, null); + return GLib.SOURCE_CONTINUE; + } + + _activeApChanged() { + if (this._activeNetwork) + this._activeNetwork.item.setActive(false); + + this._activeNetwork = null; + if (this._device.active_access_point) { + let idx = this._findNetwork(this._device.active_access_point); + if (idx >= 0) + this._activeNetwork = this._networks[idx]; + } + + if (this._activeNetwork) + this._activeNetwork.item.setActive(true); + this._updateSensitivity(); + } + + _updateSensitivity() { + let connectSensitive = this._client.wireless_enabled && this._selectedNetwork && (this._selectedNetwork != this._activeNetwork); + this._connectButton.reactive = connectSensitive; + this._connectButton.can_focus = connectSensitive; + } + + _syncView() { + if (this._rfkill.airplaneMode) { + this._airplaneBox.show(); + + this._airplaneIcon.icon_name = 'airplane-mode-symbolic'; + this._airplaneHeadline.text = _("Airplane Mode is On"); + this._airplaneText.text = _("Wi-Fi is disabled when airplane mode is on."); + this._airplaneButton.label = _("Turn Off Airplane Mode"); + + this._airplaneButton.visible = !this._rfkill.hwAirplaneMode; + this._airplaneInactive.visible = this._rfkill.hwAirplaneMode; + this._noNetworksBox.hide(); + } else if (!this._client.wireless_enabled) { + this._airplaneBox.show(); + + this._airplaneIcon.icon_name = 'dialog-information-symbolic'; + this._airplaneHeadline.text = _("Wi-Fi is Off"); + this._airplaneText.text = _("Wi-Fi needs to be turned on in order to connect to a network."); + this._airplaneButton.label = _("Turn On Wi-Fi"); + + this._airplaneButton.show(); + this._airplaneInactive.hide(); + this._noNetworksBox.hide(); + } else { + this._airplaneBox.hide(); + + this._noNetworksBox.visible = this._networks.length == 0; + } + + if (this._noNetworksBox.visible) + this._noNetworksSpinner.play(); + else + this._noNetworksSpinner.stop(); + } + + _buildLayout() { + let headline = new St.BoxLayout({ style_class: 'nm-dialog-header-hbox' }); + + let icon = new St.Icon({ style_class: 'nm-dialog-header-icon', + icon_name: 'network-wireless-signal-excellent-symbolic' }); + + let titleBox = new St.BoxLayout({ vertical: true }); + let title = new St.Label({ style_class: 'nm-dialog-header', + text: _("Wi-Fi Networks") }); + let subtitle = new St.Label({ style_class: 'nm-dialog-subheader', + text: _("Select a network") }); + titleBox.add(title); + titleBox.add(subtitle); + + headline.add(icon); + headline.add(titleBox); + + this.contentLayout.style_class = 'nm-dialog-content'; + this.contentLayout.add(headline); + + this._stack = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + y_expand: true, + }); + + this._itemBox = new St.BoxLayout({ vertical: true }); + this._scrollView = new St.ScrollView({ style_class: 'nm-dialog-scroll-view' }); + this._scrollView.set_x_expand(true); + this._scrollView.set_y_expand(true); + this._scrollView.set_policy(St.PolicyType.NEVER, + St.PolicyType.AUTOMATIC); + this._scrollView.add_actor(this._itemBox); + this._stack.add_child(this._scrollView); + + this._noNetworksBox = new St.BoxLayout({ vertical: true, + style_class: 'no-networks-box', + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER }); + + this._noNetworksSpinner = new Animation.Spinner(16); + this._noNetworksBox.add_actor(this._noNetworksSpinner); + this._noNetworksBox.add_actor(new St.Label({ style_class: 'no-networks-label', + text: _("No Networks") })); + this._stack.add_child(this._noNetworksBox); + + this._airplaneBox = new St.BoxLayout({ vertical: true, + style_class: 'nm-dialog-airplane-box', + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER }); + this._airplaneIcon = new St.Icon({ icon_size: 48 }); + this._airplaneHeadline = new St.Label({ style_class: 'nm-dialog-airplane-headline headline' }); + this._airplaneText = new St.Label({ style_class: 'nm-dialog-airplane-text' }); + + let airplaneSubStack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); + this._airplaneButton = new St.Button({ style_class: 'modal-dialog-button button' }); + this._airplaneButton.connect('clicked', () => { + if (this._rfkill.airplaneMode) + this._rfkill.airplaneMode = false; + else + this._client.wireless_enabled = true; + }); + airplaneSubStack.add_actor(this._airplaneButton); + this._airplaneInactive = new St.Label({ style_class: 'nm-dialog-airplane-text', + text: _("Use hardware switch to turn off") }); + airplaneSubStack.add_actor(this._airplaneInactive); + + this._airplaneBox.add_child(this._airplaneIcon); + this._airplaneBox.add_child(this._airplaneHeadline); + this._airplaneBox.add_child(this._airplaneText); + this._airplaneBox.add_child(airplaneSubStack); + this._stack.add_child(this._airplaneBox); + + this.contentLayout.add_child(this._stack); + + this._disconnectButton = this.addButton({ action: () => this.close(), + label: _("Cancel"), + key: Clutter.KEY_Escape }); + this._connectButton = this.addButton({ action: this._connect.bind(this), + label: _("Connect"), + key: Clutter.KEY_Return }); + } + + _connect() { + let network = this._selectedNetwork; + if (network.connections.length > 0) { + let connection = network.connections[0]; + this._client.activate_connection_async(connection, this._device, null, null, null); + } else { + let accessPoints = network.accessPoints; + if ((accessPoints[0]._secType == NMAccessPointSecurity.WPA2_ENT) || + (accessPoints[0]._secType == NMAccessPointSecurity.WPA_ENT)) { + // 802.1x-enabled APs require further configuration, so they're + // handled in gnome-control-center + launchSettingsPanel('wifi', 'connect-8021x-wifi', + this._device.get_path(), accessPoints[0].get_path()); + } else { + let connection = new NM.SimpleConnection(); + this._client.add_and_activate_connection_async(connection, this._device, accessPoints[0].get_path(), null, null); + } + } + + this.close(); + } + + _notifySsidCb(accessPoint) { + if (accessPoint.get_ssid() != null) { + accessPoint.disconnect(accessPoint._notifySsidId); + accessPoint._notifySsidId = 0; + this._accessPointAdded(this._device, accessPoint); + } + } + + _getApSecurityType(accessPoint) { + if (accessPoint._secType) + return accessPoint._secType; + + let flags = accessPoint.flags; + let wpaFlags = accessPoint.wpa_flags; + let rsnFlags = accessPoint.rsn_flags; + let type; + if (rsnFlags != NM80211ApSecurityFlags.NONE) { + /* RSN check first so that WPA+WPA2 APs are treated as RSN/WPA2 */ + if (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X) + type = NMAccessPointSecurity.WPA2_ENT; + else if (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_PSK) + type = NMAccessPointSecurity.WPA2_PSK; + } else if (wpaFlags != NM80211ApSecurityFlags.NONE) { + if (wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X) + type = NMAccessPointSecurity.WPA_ENT; + else if (wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_PSK) + type = NMAccessPointSecurity.WPA_PSK; + } else { + // eslint-disable-next-line no-lonely-if + if (flags & NM80211ApFlags.PRIVACY) + type = NMAccessPointSecurity.WEP; + else + type = NMAccessPointSecurity.NONE; + } + + // cache the found value to avoid checking flags all the time + accessPoint._secType = type; + return type; + } + + _networkSortFunction(one, two) { + let oneHasConnection = one.connections.length != 0; + let twoHasConnection = two.connections.length != 0; + + // place known connections first + // (-1 = good order, 1 = wrong order) + if (oneHasConnection && !twoHasConnection) + return -1; + else if (!oneHasConnection && twoHasConnection) + return 1; + + let oneAp = one.accessPoints[0] || null; + let twoAp = two.accessPoints[0] || null; + + if (oneAp != null && twoAp == null) + return -1; + else if (oneAp == null && twoAp != null) + return 1; + + let oneStrength = oneAp.strength; + let twoStrength = twoAp.strength; + + // place stronger connections first + if (oneStrength != twoStrength) + return oneStrength < twoStrength ? 1 : -1; + + let oneHasSecurity = one.security != NMAccessPointSecurity.NONE; + let twoHasSecurity = two.security != NMAccessPointSecurity.NONE; + + // place secure connections first + // (we treat WEP/WPA/WPA2 the same as there is no way to + // take them apart from the UI) + if (oneHasSecurity && !twoHasSecurity) + return -1; + else if (!oneHasSecurity && twoHasSecurity) + return 1; + + // sort alphabetically + return GLib.utf8_collate(one.ssidText, two.ssidText); + } + + _networkCompare(network, accessPoint) { + if (!network.ssid.equal(accessPoint.get_ssid())) + return false; + if (network.mode != accessPoint.mode) + return false; + if (network.security != this._getApSecurityType(accessPoint)) + return false; + + return true; + } + + _findExistingNetwork(accessPoint) { + for (let i = 0; i < this._networks.length; i++) { + let network = this._networks[i]; + for (let j = 0; j < network.accessPoints.length; j++) { + if (network.accessPoints[j] == accessPoint) + return { network: i, ap: j }; + } + } + + return null; + } + + _findNetwork(accessPoint) { + if (accessPoint.get_ssid() == null) + return -1; + + for (let i = 0; i < this._networks.length; i++) { + if (this._networkCompare(this._networks[i], accessPoint)) + return i; + } + return -1; + } + + _checkConnections(network, accessPoint) { + this._connections.forEach(connection => { + if (accessPoint.connection_valid(connection) && + !network.connections.includes(connection)) + network.connections.push(connection); + }); + } + + _accessPointAdded(device, accessPoint) { + if (accessPoint.get_ssid() == null) { + // This access point is not visible yet + // Wait for it to get a ssid + accessPoint._notifySsidId = accessPoint.connect('notify::ssid', this._notifySsidCb.bind(this)); + return; + } + + let pos = this._findNetwork(accessPoint); + let network; + + if (pos != -1) { + network = this._networks[pos]; + if (network.accessPoints.includes(accessPoint)) { + log('Access point was already seen, not adding again'); + return; + } + + Util.insertSorted(network.accessPoints, accessPoint, (one, two) => { + return two.strength - one.strength; + }); + network.item.updateBestAP(network.accessPoints[0]); + this._checkConnections(network, accessPoint); + + this._resortItems(); + } else { + network = { + ssid: accessPoint.get_ssid(), + mode: accessPoint.mode, + security: this._getApSecurityType(accessPoint), + connections: [], + item: null, + accessPoints: [accessPoint], + }; + network.ssidText = ssidToLabel(network.ssid); + this._checkConnections(network, accessPoint); + + let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction); + this._createNetworkItem(network); + this._itemBox.insert_child_at_index(network.item, newPos); + } + + this._syncView(); + } + + _accessPointRemoved(device, accessPoint) { + let res = this._findExistingNetwork(accessPoint); + + if (res == null) { + log('Removing an access point that was never added'); + return; + } + + let network = this._networks[res.network]; + network.accessPoints.splice(res.ap, 1); + + if (network.accessPoints.length == 0) { + network.item.destroy(); + this._networks.splice(res.network, 1); + } else { + network.item.updateBestAP(network.accessPoints[0]); + this._resortItems(); + } + + this._syncView(); + } + + _resortItems() { + let adjustment = this._scrollView.vscroll.adjustment; + let scrollValue = adjustment.value; + + this._itemBox.remove_all_children(); + this._networks.forEach(network => { + this._itemBox.add_child(network.item); + }); + + adjustment.value = scrollValue; + } + + _selectNetwork(network) { + if (this._selectedNetwork) + this._selectedNetwork.item.remove_style_pseudo_class('selected'); + + this._selectedNetwork = network; + this._updateSensitivity(); + + if (this._selectedNetwork) + this._selectedNetwork.item.add_style_pseudo_class('selected'); + } + + _createNetworkItem(network) { + network.item = new NMWirelessDialogItem(network); + network.item.setActive(network == this._selectedNetwork); + network.item.connect('selected', () => { + Util.ensureActorVisibleInScrollView(this._scrollView, network.item); + this._selectNetwork(network); + }); + network.item.connect('destroy', () => { + let keyFocus = global.stage.key_focus; + if (keyFocus && keyFocus.contains(network.item)) + this._itemBox.grab_key_focus(); + }); + } +}); + +var NMDeviceWireless = class { + constructor(client, device) { + this._client = client; + this._device = device; + + this._description = ''; + + this.item = new PopupMenu.PopupSubMenuMenuItem('', true); + this.item.menu.addAction(_("Select Network"), this._showDialog.bind(this)); + + this._toggleItem = new PopupMenu.PopupMenuItem(''); + this._toggleItem.connect('activate', this._toggleWifi.bind(this)); + this.item.menu.addMenuItem(this._toggleItem); + + this.item.menu.addSettingsAction(_("Wi-Fi Settings"), 'gnome-wifi-panel.desktop'); + + this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled', this._sync.bind(this)); + this._wirelessHwEnabledChangedId = this._client.connect('notify::wireless-hardware-enabled', this._sync.bind(this)); + this._activeApChangedId = this._device.connect('notify::active-access-point', this._activeApChanged.bind(this)); + this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this)); + this._notifyConnectivityId = this._client.connect('notify::connectivity', this._iconChanged.bind(this)); + + this._sync(); + } + + get category() { + return NMConnectionCategory.WIRELESS; + } + + _iconChanged() { + this._sync(); + this.emit('icon-changed'); + } + + destroy() { + if (this._activeApChangedId) { + GObject.signal_handler_disconnect(this._device, this._activeApChangedId); + this._activeApChangedId = 0; + } + if (this._stateChangedId) { + GObject.signal_handler_disconnect(this._device, this._stateChangedId); + this._stateChangedId = 0; + } + if (this._strengthChangedId > 0) { + this._activeAccessPoint.disconnect(this._strengthChangedId); + this._strengthChangedId = 0; + } + if (this._wirelessEnabledChangedId) { + this._client.disconnect(this._wirelessEnabledChangedId); + this._wirelessEnabledChangedId = 0; + } + if (this._wirelessHwEnabledChangedId) { + this._client.disconnect(this._wirelessHwEnabledChangedId); + this._wirelessHwEnabledChangedId = 0; + } + if (this._dialog) { + this._dialog.destroy(); + this._dialog = null; + } + if (this._notifyConnectivityId) { + this._client.disconnect(this._notifyConnectivityId); + this._notifyConnectivityId = 0; + } + + this.item.destroy(); + } + + _deviceStateChanged(device, newstate, oldstate, reason) { + if (newstate == oldstate) { + log('device emitted state-changed without actually changing state'); + return; + } + + /* Emit a notification if activation fails, but don't do it + if the reason is no secrets, as that indicates the user + cancelled the agent dialog */ + if (newstate == NM.DeviceState.FAILED && + reason != NM.DeviceStateReason.NO_SECRETS) + this.emit('activation-failed', reason); + + this._sync(); + } + + _toggleWifi() { + this._client.wireless_enabled = !this._client.wireless_enabled; + } + + _showDialog() { + this._dialog = new NMWirelessDialog(this._client, this._device); + this._dialog.connect('closed', this._dialogClosed.bind(this)); + this._dialog.open(); + } + + _dialogClosed() { + this._dialog = null; + } + + _strengthChanged() { + this._iconChanged(); + } + + _activeApChanged() { + if (this._activeAccessPoint) { + this._activeAccessPoint.disconnect(this._strengthChangedId); + this._strengthChangedId = 0; + } + + this._activeAccessPoint = this._device.active_access_point; + + if (this._activeAccessPoint) { + this._strengthChangedId = this._activeAccessPoint.connect('notify::strength', + this._strengthChanged.bind(this)); + } + + this._sync(); + } + + _sync() { + this._toggleItem.label.text = this._client.wireless_enabled ? _("Turn Off") : _("Turn On"); + this._toggleItem.visible = this._client.wireless_hardware_enabled; + + this.item.icon.icon_name = this._getMenuIcon(); + this.item.label.text = this._getStatus(); + } + + setDeviceDescription(desc) { + this._description = desc; + this._sync(); + } + + _getStatus() { + let ap = this._device.active_access_point; + + if (this._isHotSpotMaster()) + /* Translators: %s is a network identifier */ + return _("%s Hotspot Active").format(this._description); + else if (this._device.state >= NM.DeviceState.PREPARE && + this._device.state < NM.DeviceState.ACTIVATED) + /* Translators: %s is a network identifier */ + return _("%s Connecting").format(this._description); + else if (ap) + return ssidToLabel(ap.get_ssid()); + else if (!this._client.wireless_hardware_enabled) + /* Translators: %s is a network identifier */ + return _("%s Hardware Disabled").format(this._description); + else if (!this._client.wireless_enabled) + /* Translators: %s is a network identifier */ + return _("%s Off").format(this._description); + else if (this._device.state == NM.DeviceState.DISCONNECTED) + /* Translators: %s is a network identifier */ + return _("%s Not Connected").format(this._description); + else + return ''; + } + + _getMenuIcon() { + if (this._device.active_connection) + return this.getIndicatorIcon(); + else + return 'network-wireless-signal-none-symbolic'; + } + + _canReachInternet() { + if (this._client.primary_connection != this._device.active_connection) + return true; + + return this._client.connectivity == NM.ConnectivityState.FULL; + } + + _isHotSpotMaster() { + if (!this._device.active_connection) + return false; + + let connection = this._device.active_connection.connection; + if (!connection) + return false; + + let ip4config = connection.get_setting_ip4_config(); + if (!ip4config) + return false; + + return ip4config.get_method() == NM.SETTING_IP4_CONFIG_METHOD_SHARED; + } + + getIndicatorIcon() { + if (this._device.state < NM.DeviceState.PREPARE) + return 'network-wireless-disconnected-symbolic'; + if (this._device.state < NM.DeviceState.ACTIVATED) + return 'network-wireless-acquiring-symbolic'; + + if (this._isHotSpotMaster()) + return 'network-wireless-hotspot-symbolic'; + + let ap = this._device.active_access_point; + if (!ap) { + if (this._device.mode != NM80211Mode.ADHOC) + log('An active wireless connection, in infrastructure mode, involves no access point?'); + + if (this._canReachInternet()) + return 'network-wireless-connected-symbolic'; + else + return 'network-wireless-no-route-symbolic'; + } + + if (this._canReachInternet()) + return 'network-wireless-signal-%s-symbolic'.format(signalToIcon(ap.strength)); + else + return 'network-wireless-no-route-symbolic'; + } +}; +Signals.addSignalMethods(NMDeviceWireless.prototype); + +var NMVpnConnectionItem = class extends NMConnectionItem { + isActive() { + if (this._activeConnection == null) + return false; + + return this._activeConnection.vpn_state != NM.VpnConnectionState.DISCONNECTED; + } + + _buildUI() { + this.labelItem = new PopupMenu.PopupMenuItem(''); + this.labelItem.connect('activate', this._toggle.bind(this)); + + this.radioItem = new PopupMenu.PopupSwitchMenuItem(this._connection.get_id(), false); + this.radioItem.connect('toggled', this._toggle.bind(this)); + } + + _sync() { + let isActive = this.isActive(); + this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel(); + this.radioItem.setToggleState(isActive); + this.radioItem.setStatus(this._getStatus()); + this.emit('icon-changed'); + } + + _getStatus() { + if (this._activeConnection == null) + return null; + + switch (this._activeConnection.vpn_state) { + case NM.VpnConnectionState.DISCONNECTED: + case NM.VpnConnectionState.ACTIVATED: + return null; + case NM.VpnConnectionState.PREPARE: + case NM.VpnConnectionState.CONNECT: + case NM.VpnConnectionState.IP_CONFIG_GET: + return _("connecting…"); + case NM.VpnConnectionState.NEED_AUTH: + /* Translators: this is for network connections that require some kind of key or password */ + return _("authentication required"); + case NM.VpnConnectionState.FAILED: + return _("connection failed"); + default: + return 'invalid'; + } + } + + _connectionStateChanged(ac, newstate, reason) { + if (newstate == NM.VpnConnectionState.FAILED && + reason != NM.VpnConnectionStateReason.NO_SECRETS) { + // FIXME: if we ever want to show something based on reason, + // we need to convert from NM.VpnConnectionStateReason + // to NM.DeviceStateReason + this.emit('activation-failed', reason); + } + + this.emit('icon-changed'); + super._connectionStateChanged(); + } + + setActiveConnection(activeConnection) { + if (this._activeConnectionChangedId > 0) { + this._activeConnection.disconnect(this._activeConnectionChangedId); + this._activeConnectionChangedId = 0; + } + + this._activeConnection = activeConnection; + + if (this._activeConnection) { + this._activeConnectionChangedId = this._activeConnection.connect('vpn-state-changed', + this._connectionStateChanged.bind(this)); + } + + this._sync(); + } + + getIndicatorIcon() { + if (this._activeConnection) { + if (this._activeConnection.vpn_state < NM.VpnConnectionState.ACTIVATED) + return 'network-vpn-acquiring-symbolic'; + else + return 'network-vpn-symbolic'; + } else { + return ''; + } + } +}; + +var NMVpnSection = class extends NMConnectionSection { + constructor(client) { + super(client); + + this.item.menu.addSettingsAction(_("VPN Settings"), 'gnome-network-panel.desktop'); + + this._sync(); + } + + _sync() { + let nItems = this._connectionItems.size; + this.item.visible = nItems > 0; + + super._sync(); + } + + get category() { + return NMConnectionCategory.VPN; + } + + _getDescription() { + return _("VPN"); + } + + _getStatus() { + let values = this._connectionItems.values(); + for (let item of values) { + if (item.isActive()) + return item.getName(); + } + + return _("VPN Off"); + } + + _getMenuIcon() { + return this.getIndicatorIcon() || 'network-vpn-symbolic'; + } + + activateConnection(connection) { + this._client.activate_connection_async(connection, null, null, null, null); + } + + deactivateConnection(activeConnection) { + this._client.deactivate_connection(activeConnection, null); + } + + setActiveConnections(vpnConnections) { + let connections = this._connectionItems.values(); + for (let item of connections) + item.setActiveConnection(null); + + vpnConnections.forEach(a => { + if (a.connection) { + let item = this._connectionItems.get(a.connection.get_uuid()); + item.setActiveConnection(a); + } + }); + } + + _makeConnectionItem(connection) { + return new NMVpnConnectionItem(this, connection); + } + + getIndicatorIcon() { + let items = this._connectionItems.values(); + for (let item of items) { + let icon = item.getIndicatorIcon(); + if (icon) + return icon; + } + return ''; + } +}; +Signals.addSignalMethods(NMVpnSection.prototype); + +var DeviceCategory = class extends PopupMenu.PopupMenuSection { + constructor(category) { + super(); + + this._category = category; + + this.devices = []; + + this.section = new PopupMenu.PopupMenuSection(); + this.section.box.connect('actor-added', this._sync.bind(this)); + this.section.box.connect('actor-removed', this._sync.bind(this)); + this.addMenuItem(this.section); + + this._summaryItem = new PopupMenu.PopupSubMenuMenuItem('', true); + this._summaryItem.icon.icon_name = this._getSummaryIcon(); + this.addMenuItem(this._summaryItem); + + this._summaryItem.menu.addSettingsAction(_('Network Settings'), + 'gnome-network-panel.desktop'); + this._summaryItem.hide(); + + } + + _sync() { + let nDevices = this.section.box.get_children().reduce( + (prev, child) => prev + (child.visible ? 1 : 0), 0); + this._summaryItem.label.text = this._getSummaryLabel(nDevices); + let shouldSummarize = nDevices > MAX_DEVICE_ITEMS; + this._summaryItem.visible = shouldSummarize; + this.section.actor.visible = !shouldSummarize; + } + + _getSummaryIcon() { + switch (this._category) { + case NMConnectionCategory.WIRED: + return 'network-wired-symbolic'; + case NMConnectionCategory.WIRELESS: + case NMConnectionCategory.WWAN: + return 'network-wireless-symbolic'; + } + return ''; + } + + _getSummaryLabel(nDevices) { + switch (this._category) { + case NMConnectionCategory.WIRED: + return ngettext("%s Wired Connection", + "%s Wired Connections", + nDevices).format(nDevices); + case NMConnectionCategory.WIRELESS: + return ngettext("%s Wi-Fi Connection", + "%s Wi-Fi Connections", + nDevices).format(nDevices); + case NMConnectionCategory.WWAN: + return ngettext("%s Modem Connection", + "%s Modem Connections", + nDevices).format(nDevices); + } + return ''; + } +}; + +var NMApplet = GObject.registerClass( +class Indicator extends PanelMenu.SystemIndicator { + _init() { + super._init(); + + this._primaryIndicator = this._addIndicator(); + this._vpnIndicator = this._addIndicator(); + + // Device types + this._dtypes = { }; + this._dtypes[NM.DeviceType.ETHERNET] = NMDeviceWired; + this._dtypes[NM.DeviceType.WIFI] = NMDeviceWireless; + this._dtypes[NM.DeviceType.MODEM] = NMDeviceModem; + this._dtypes[NM.DeviceType.BT] = NMDeviceBluetooth; + + // Connection types + this._ctypes = { }; + this._ctypes[NM.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED; + this._ctypes[NM.SETTING_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS; + this._ctypes[NM.SETTING_BLUETOOTH_SETTING_NAME] = NMConnectionCategory.WWAN; + this._ctypes[NM.SETTING_CDMA_SETTING_NAME] = NMConnectionCategory.WWAN; + this._ctypes[NM.SETTING_GSM_SETTING_NAME] = NMConnectionCategory.WWAN; + this._ctypes[NM.SETTING_VPN_SETTING_NAME] = NMConnectionCategory.VPN; + + this._getClient(); + } + + async _getClient() { + this._client = await NM.Client.new_async(null); + + this._activeConnections = []; + this._connections = []; + this._connectivityQueue = []; + + this._mainConnection = null; + this._mainConnectionIconChangedId = 0; + this._mainConnectionStateChangedId = 0; + + this._notification = null; + + this._nmDevices = []; + this._devices = { }; + + let categories = [NMConnectionCategory.WIRED, + NMConnectionCategory.WIRELESS, + NMConnectionCategory.WWAN]; + for (let category of categories) { + this._devices[category] = new DeviceCategory(category); + this.menu.addMenuItem(this._devices[category]); + } + + this._vpnSection = new NMVpnSection(this._client); + this._vpnSection.connect('activation-failed', this._onActivationFailed.bind(this)); + this._vpnSection.connect('icon-changed', this._updateIcon.bind(this)); + this.menu.addMenuItem(this._vpnSection.item); + + this._readConnections(); + this._readDevices(); + this._syncNMState(); + this._syncMainConnection(); + this._syncVpnConnections(); + + this._client.connect('notify::nm-running', this._syncNMState.bind(this)); + this._client.connect('notify::networking-enabled', this._syncNMState.bind(this)); + this._client.connect('notify::state', this._syncNMState.bind(this)); + this._client.connect('notify::primary-connection', this._syncMainConnection.bind(this)); + this._client.connect('notify::activating-connection', this._syncMainConnection.bind(this)); + this._client.connect('notify::active-connections', this._syncVpnConnections.bind(this)); + this._client.connect('notify::connectivity', this._syncConnectivity.bind(this)); + this._client.connect('device-added', this._deviceAdded.bind(this)); + this._client.connect('device-removed', this._deviceRemoved.bind(this)); + this._client.connect('connection-added', this._connectionAdded.bind(this)); + this._client.connect('connection-removed', this._connectionRemoved.bind(this)); + + Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); + this._sessionUpdated(); + } + + _sessionUpdated() { + let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter; + this.menu.setSensitive(sensitive); + } + + _ensureSource() { + if (!this._source) { + this._source = new MessageTray.Source(_("Network Manager"), + 'network-transmit-receive'); + this._source.policy = new MessageTray.NotificationApplicationPolicy('gnome-network-panel'); + + this._source.connect('destroy', () => (this._source = null)); + Main.messageTray.add(this._source); + } + } + + _readDevices() { + let devices = this._client.get_devices() || []; + for (let i = 0; i < devices.length; ++i) { + try { + this._deviceAdded(this._client, devices[i], true); + } catch (e) { + log('Failed to add device %s: %s'.format(devices[i], e.toString())); + } + } + this._syncDeviceNames(); + } + + _notify(iconName, title, text, urgency) { + if (this._notification) + this._notification.destroy(); + + this._ensureSource(); + + let gicon = new Gio.ThemedIcon({ name: iconName }); + this._notification = new MessageTray.Notification(this._source, title, text, { gicon }); + this._notification.setUrgency(urgency); + this._notification.setTransient(true); + this._notification.connect('destroy', () => { + this._notification = null; + }); + this._source.showNotification(this._notification); + } + + _onActivationFailed(_device, _reason) { + // XXX: nm-applet has no special text depending on reason + // but I'm not sure of this generic message + this._notify('network-error-symbolic', + _("Connection failed"), + _("Activation of network connection failed"), + MessageTray.Urgency.HIGH); + } + + _syncDeviceNames() { + let names = NM.Device.disambiguate_names(this._nmDevices); + for (let i = 0; i < this._nmDevices.length; i++) { + let device = this._nmDevices[i]; + let description = names[i]; + if (device._delegate) + device._delegate.setDeviceDescription(description); + } + } + + _deviceAdded(client, device, skipSyncDeviceNames) { + if (device._delegate) { + // already seen, not adding again + return; + } + + let wrapperClass = this._dtypes[device.get_device_type()]; + if (wrapperClass) { + let wrapper = new wrapperClass(this._client, device); + device._delegate = wrapper; + this._addDeviceWrapper(wrapper); + + this._nmDevices.push(device); + this._deviceChanged(device, skipSyncDeviceNames); + + device.connect('notify::interface', () => { + this._deviceChanged(device, false); + }); + } + } + + _deviceChanged(device, skipSyncDeviceNames) { + let wrapper = device._delegate; + + if (!skipSyncDeviceNames) + this._syncDeviceNames(); + + if (wrapper instanceof NMConnectionSection) { + this._connections.forEach(connection => { + wrapper.checkConnection(connection); + }); + } + } + + _addDeviceWrapper(wrapper) { + wrapper._activationFailedId = wrapper.connect('activation-failed', + this._onActivationFailed.bind(this)); + + let section = this._devices[wrapper.category].section; + section.addMenuItem(wrapper.item); + + let devices = this._devices[wrapper.category].devices; + devices.push(wrapper); + } + + _deviceRemoved(client, device) { + let pos = this._nmDevices.indexOf(device); + if (pos != -1) { + this._nmDevices.splice(pos, 1); + this._syncDeviceNames(); + } + + let wrapper = device._delegate; + if (!wrapper) { + log('Removing a network device that was not added'); + return; + } + + this._removeDeviceWrapper(wrapper); + } + + _removeDeviceWrapper(wrapper) { + wrapper.disconnect(wrapper._activationFailedId); + wrapper.destroy(); + + let devices = this._devices[wrapper.category].devices; + let pos = devices.indexOf(wrapper); + devices.splice(pos, 1); + } + + _getMainConnection() { + let connection; + + connection = this._client.get_primary_connection(); + if (connection) { + ensureActiveConnectionProps(connection); + return connection; + } + + connection = this._client.get_activating_connection(); + if (connection) { + ensureActiveConnectionProps(connection); + return connection; + } + + return null; + } + + _syncMainConnection() { + if (this._mainConnectionIconChangedId > 0) { + this._mainConnection._primaryDevice.disconnect(this._mainConnectionIconChangedId); + this._mainConnectionIconChangedId = 0; + } + + if (this._mainConnectionStateChangedId > 0) { + this._mainConnection.disconnect(this._mainConnectionStateChangedId); + this._mainConnectionStateChangedId = 0; + } + + this._mainConnection = this._getMainConnection(); + + if (this._mainConnection) { + if (this._mainConnection._primaryDevice) + this._mainConnectionIconChangedId = this._mainConnection._primaryDevice.connect('icon-changed', this._updateIcon.bind(this)); + this._mainConnectionStateChangedId = this._mainConnection.connect('notify::state', this._mainConnectionStateChanged.bind(this)); + this._mainConnectionStateChanged(); + } + + this._updateIcon(); + this._syncConnectivity(); + } + + _syncVpnConnections() { + let activeConnections = this._client.get_active_connections() || []; + let vpnConnections = activeConnections.filter( + a => a instanceof NM.VpnConnection); + vpnConnections.forEach(a => { + ensureActiveConnectionProps(a); + }); + this._vpnSection.setActiveConnections(vpnConnections); + + this._updateIcon(); + } + + _mainConnectionStateChanged() { + if (this._mainConnection.state == NM.ActiveConnectionState.ACTIVATED && this._notification) + this._notification.destroy(); + } + + _ignoreConnection(connection) { + let setting = connection.get_setting_connection(); + if (!setting) + return true; + + // Ignore slave connections + if (setting.get_master()) + return true; + + return false; + } + + _addConnection(connection) { + if (this._ignoreConnection(connection)) + return; + if (connection._updatedId) { + // connection was already seen + return; + } + + connection._updatedId = connection.connect('changed', this._updateConnection.bind(this)); + + this._updateConnection(connection); + this._connections.push(connection); + } + + _readConnections() { + let connections = this._client.get_connections(); + connections.forEach(this._addConnection.bind(this)); + } + + _connectionAdded(client, connection) { + this._addConnection(connection); + } + + _connectionRemoved(client, connection) { + let pos = this._connections.indexOf(connection); + if (pos != -1) + this._connections.splice(pos, 1); + + let section = connection._section; + + if (section == NMConnectionCategory.INVALID) + return; + + if (section == NMConnectionCategory.VPN) { + this._vpnSection.removeConnection(connection); + } else { + let devices = this._devices[section].devices; + for (let i = 0; i < devices.length; i++) { + if (devices[i] instanceof NMConnectionSection) + devices[i].removeConnection(connection); + } + } + + connection.disconnect(connection._updatedId); + connection._updatedId = 0; + } + + _updateConnection(connection) { + let connectionSettings = connection.get_setting_by_name(NM.SETTING_CONNECTION_SETTING_NAME); + connection._type = connectionSettings.type; + connection._section = this._ctypes[connection._type] || NMConnectionCategory.INVALID; + + let section = connection._section; + + if (section == NMConnectionCategory.INVALID) + return; + + if (section == NMConnectionCategory.VPN) { + this._vpnSection.checkConnection(connection); + } else { + let devices = this._devices[section].devices; + devices.forEach(wrapper => { + if (wrapper instanceof NMConnectionSection) + wrapper.checkConnection(connection); + }); + } + } + + _syncNMState() { + this.visible = this._client.nm_running; + this.menu.actor.visible = this._client.networking_enabled; + + this._updateIcon(); + this._syncConnectivity(); + } + + _flushConnectivityQueue() { + if (this._portalHelperProxy) { + for (let item of this._connectivityQueue) + this._portalHelperProxy.CloseRemote(item); + } + + this._connectivityQueue = []; + } + + _closeConnectivityCheck(path) { + let index = this._connectivityQueue.indexOf(path); + + if (index >= 0) { + if (this._portalHelperProxy) + this._portalHelperProxy.CloseRemote(path); + + this._connectivityQueue.splice(index, 1); + } + } + + async _portalHelperDone(proxy, emitter, parameters) { + let [path, result] = parameters; + + if (result == PortalHelperResult.CANCELLED) { + // Keep the connection in the queue, so the user is not + // spammed with more logins until we next flush the queue, + // which will happen once he chooses a better connection + // or we get to full connectivity through other means + } else if (result == PortalHelperResult.COMPLETED) { + this._closeConnectivityCheck(path); + } else if (result == PortalHelperResult.RECHECK) { + try { + const state = await this._client.check_connectivity_async(null); + if (state >= NM.ConnectivityState.FULL) + this._closeConnectivityCheck(path); + } catch (e) { } + } else { + log('Invalid result from portal helper: %s'.format(result)); + } + } + + _syncConnectivity() { + if (this._mainConnection == null || + this._mainConnection.state != NM.ActiveConnectionState.ACTIVATED) { + this._flushConnectivityQueue(); + return; + } + + let isPortal = this._client.connectivity == NM.ConnectivityState.PORTAL; + // For testing, allow interpreting any value != FULL as PORTAL, because + // LIMITED (no upstream route after the default gateway) is easy to obtain + // with a tethered phone + // NONE is also possible, with a connection configured to force no default route + // (but in general we should only prompt a portal if we know there is a portal) + if (GLib.getenv('GNOME_SHELL_CONNECTIVITY_TEST') != null) + isPortal = isPortal || this._client.connectivity < NM.ConnectivityState.FULL; + if (!isPortal || Main.sessionMode.isGreeter) + return; + + let path = this._mainConnection.get_path(); + for (let item of this._connectivityQueue) { + if (item == path) + return; + } + + let timestamp = global.get_current_time(); + if (this._portalHelperProxy) { + this._portalHelperProxy.AuthenticateRemote(path, '', timestamp); + } else { + new PortalHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PortalHelper', + '/org/gnome/Shell/PortalHelper', (proxy, error) => { + if (error) { + log('Error launching the portal helper: %s'.format(error)); + return; + } + + this._portalHelperProxy = proxy; + proxy.connectSignal('Done', this._portalHelperDone.bind(this)); + + proxy.AuthenticateRemote(path, '', timestamp); + }); + } + + this._connectivityQueue.push(path); + } + + _updateIcon() { + if (!this._client.networking_enabled) { + this._primaryIndicator.visible = false; + } else { + let dev = null; + if (this._mainConnection) + dev = this._mainConnection._primaryDevice; + + let state = this._client.get_state(); + let connected = state == NM.State.CONNECTED_GLOBAL; + this._primaryIndicator.visible = (dev != null) || connected; + if (dev) { + this._primaryIndicator.icon_name = dev.getIndicatorIcon(); + } else if (connected) { + if (this._client.connectivity == NM.ConnectivityState.FULL) + this._primaryIndicator.icon_name = 'network-wired-symbolic'; + else + this._primaryIndicator.icon_name = 'network-wired-no-route-symbolic'; + } + } + + this._vpnIndicator.icon_name = this._vpnSection.getIndicatorIcon(); + this._vpnIndicator.visible = this._vpnIndicator.icon_name !== null; + } +}); |