summaryrefslogtreecommitdiffstats
path: root/js/ui/status/network.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/status/network.js')
-rw-r--r--js/ui/status/network.js2101
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;
+ }
+});