const Applet = imports.ui.applet;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Lang = imports.lang;
const NM = imports.gi.NM;
const Signals = imports.signals;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const MessageTray = imports.ui.messageTray;
const ModemManager = imports.misc.modemManager;
const Util = imports.misc.util;
const Settings = imports.ui.settings;
const DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS = 10;
const FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS = 2;
const NMConnectionCategory = {
INVALID: 'invalid',
WIRED: 'wired',
WIRELESS: 'wireless',
WWAN: 'wwan',
VPN: 'vpn',
WIREGUARD: 'wireguard'
};
const NMAccessPointSecurity = {
UNKNOWN: 0,
NONE: 1,
WEP: 2,
WPA_PSK: 3,
WPA2_PSK: 4,
WPA_ENT: 5,
WPA2_ENT: 6
};
// small optimization, to avoid using [] all the time
const NM80211Mode = NM['80211Mode'];
const NM80211ApFlags = NM['80211ApFlags'];
const NM80211ApSecurityFlags = NM['80211ApSecurityFlags'];
// number of wireless networks that should be visible
// (the remaining are placed into More...)
const NUM_VISIBLE_NETWORKS = 5;
var NMIface = '\
\
\
\
\
\
\
';
var NMProxy = Gio.DBusProxy.makeProxyWrapper(NMIface);
function NMDBus(initCallback, cancellable) {
return new NMProxy(Gio.DBus.system,
'org.freedesktop.NetworkManager',
'/org/freedesktop/NetworkManager',
initCallback, cancellable);
}
// shared between NMNetworkMenuItem and NMDeviceWWAN
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';
}
// shared between NMNetworkMenuItem and NMDeviceWireless
function sortAccessPoints(accessPoints) {
return accessPoints.sort(function (one, two) {
return two.strength - one.strength;
});
}
function ssidToLabel(ssid) {
let label = NM.utils_ssid_to_utf8(ssid.get_data());
if (!label)
label = _("");
return label;
}
function NMNetworkMenuItem() {
this._init.apply(this, arguments);
}
function default_to_private_connections(client) {
let perms = client.get_permission_result (NM.ClientPermission.SETTINGS_MODIFY_SYSTEM);
return (perms != NM.ClientPermissionResult.YES);
}
NMNetworkMenuItem.prototype = {
__proto__: PopupMenu.PopupBaseMenuItem.prototype,
_init: function(accessPoints, title, params) {
PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
accessPoints = sortAccessPoints(accessPoints);
this.bestAP = accessPoints[0];
if (!title) {
let ssid = this.bestAP.get_ssid();
title = ssidToLabel(ssid);
}
this._label = new St.Label({ text: title });
this.addActor(this._label);
let strStrengh = String(this.bestAP.strength);
strStrengh = strStrengh + '%';
this._labelStrength = new St.Label({ text: strStrengh });
this.addActor(this._labelStrength, { align: St.Align.END });
this._icons = new St.BoxLayout({ style_class: 'nm-menu-item-icons' });
this.addActor(this._icons, { align: St.Align.END });
this._signalIcon = new St.Icon({ icon_name: this._getIcon(),
style_class: 'popup-menu-icon' });
this._icons.add_actor(this._signalIcon);
this._accessPoints = [ ];
for (let i = 0; i < accessPoints.length; i++) {
let ap = accessPoints[i];
// need a wrapper object here, because the access points can be shared
// between many NMNetworkMenuItems
let apObj = {
ap: ap,
updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
};
this._accessPoints.push(apObj);
}
},
_updated: function(ap) {
if (ap.strength > this.bestAP.strength)
this.bestAP = ap;
this._signalIcon.icon_name = this._getIcon();
let strStrengh = String(this.bestAP.strength);
strStrengh = strStrengh + '%';
this._labelStrength.set_text(strStrengh);
},
_getIcon: function() {
if (this.bestAP.mode == NM80211Mode.ADHOC)
return 'xsi-network-workgroup';
else if (this.bestAP._secType != NMAccessPointSecurity.UNKNOWN &&
this.bestAP._secType != NMAccessPointSecurity.NONE)
return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength) + '-secure';
else
return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength);
},
updateAccessPoints: function(accessPoints) {
for (let i = 0; i < this._accessPoints.length; i++) {
let apObj = this._accessPoints[i];
apObj.ap.disconnect(apObj.updateId);
apObj.updateId = 0;
}
accessPoints = sortAccessPoints(accessPoints);
this.bestAP = accessPoints[0];
this._accessPoints = [ ];
for (let i = 0; i < accessPoints.length; i++) {
let ap = accessPoints[i];
let apObj = {
ap: ap,
updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
};
this._accessPoints.push(apObj);
}
},
destroy: function() {
for (let i = 0; i < this._accessPoints.length; i++) {
let apObj = this._accessPoints[i];
apObj.ap.disconnect(apObj.updateId);
apObj.updateId = 0;
}
PopupMenu.PopupBaseMenuItem.prototype.destroy.call(this);
}
};
function NMWiredSectionTitleMenuItem() {
this._init.apply(this, arguments);
}
NMWiredSectionTitleMenuItem.prototype = {
__proto__: PopupMenu.PopupSwitchMenuItem.prototype,
_init: function(label, params) {
params = params || { };
params.style_class = 'popup-subtitle-menu-item';
PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, label, false, params);
},
updateForDevice: function(device) {
if (device) {
this._device = device;
this.setStatus(device.statusLabel);
this.setToggleState(device.connected);
// if this device is not controllable, hide the switch
this._switch.actor.visible = device.controllable;
} else {
this.setStatus('');
this._switch.actor.visible = false;
}
},
activate: function(event) {
PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);
if (!this._device) {
log('Section title activated when there is more than one device, should be non reactive');
return;
}
let newState = this._switch.state;
// Immediately reset the switch to false, it will be updated appropriately
// by state-changed signals in devices (but fixes the VPN not being in sync
// if the ActiveConnection object is never seen by libnm-glib)
this._switch.setToggleState(false);
if (newState)
this._device.activate();
else
this._device.deactivate();
}
};
function NMWirelessSectionTitleMenuItem() {
this._init.apply(this, arguments);
}
NMWirelessSectionTitleMenuItem.prototype = {
__proto__: PopupMenu.PopupSwitchMenuItem.prototype,
_init: function(client, property, title, params) {
params = params || { };
params.style_class = 'popup-subtitle-menu-item';
PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, title, false, params);
this._client = client;
this._property = property + '_enabled';
this._propertyHardware = property + '_hardware_enabled';
this._setEnabledFunc = property + '_set_enabled';
this._client.connect('notify::' + property + '-enabled', Lang.bind(this, this._propertyChanged));
this._client.connect('notify::' + property + '-hardware-enabled', Lang.bind(this, this._propertyChanged));
this._propertyChanged();
},
updateForDevice: function(device) {
this._device = device;
// we show the switch
// - if there not just one device
// - if the switch is off
// - if the device is activated or disconnected
if (device && this._softwareEnabled && this._hardwareEnabled) {
this.setStatus(device.statusLabel);
this._switch.actor.visible = device.controllable;
} else {
this.setStatus(null);
this._switch.actor.show();
}
},
activate: function(event) {
PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);
this._client[this._setEnabledFunc](this._switch.state);
if (!this._device) {
log('Section title activated when there is more than one device, should be non reactive');
return;
}
let newState = this._switch.state;
if (newState)
this._device.activate();
else
this._device.deactivate();
this.emit('enabled-changed', this._switch.state);
},
_propertyChanged: function() {
this._softwareEnabled = this._client[this._property];
this._hardwareEnabled = this._client[this._propertyHardware];
let enabled = this._softwareEnabled && this._hardwareEnabled;
this.setToggleState(enabled);
if (!this._hardwareEnabled)
/* Translators: this indicates that wireless or wwan is disabled by hardware killswitch */
this.setStatus(_("disabled"));
this.emit('enabled-changed', enabled);
}
};
function NMDevice() {
throw new TypeError('Instantanting abstract class NMDevice');
}
NMDevice.prototype = {
_init: function(client, device, connections) {
this.device = device;
if (device) {
this.device._delegate = this;
this._stateChangedId = this.device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
} else
this._stateChangedId = 0;
// protected
this._client = client;
this._connections = [ ];
for (let i = 0; i < connections.length; i++) {
if (!connections[i]._uuid)
continue;
if (!this.connectionValid(connections[i]))
continue;
// record the connection
let obj = {
connection: connections[i],
name: connections[i]._name,
uuid: connections[i]._uuid,
timestamp: connections[i]._timestamp,
};
this._connections.push(obj);
}
this._connections.sort(this._connectionSortFunction);
this._activeConnection = null;
this._activeConnectionItem = null;
this._autoConnectionItem = null;
this._overflowItem = null;
this._carrierChangedId = 0;
this._firmwareChangedId = 0;
this._firmwareMissingId = 0;
if (this.device) {
this.statusItem = new PopupMenu.PopupSwitchMenuItem(this._getDescription(), this.connected, { style_class: 'popup-subtitle-menu-item' });
this._statusChanged = this.statusItem.connect('toggled', Lang.bind(this, function(item, state) {
if (state)
this.activate();
else
this.deactivate();
this.emit('enabled-changed');
}));
this._updateStatusItem();
}
this.section = new PopupMenu.PopupMenuSection();
this._createSection();
},
destroy: function() {
if (this.device)
this.device._delegate = null;
if (this._stateChangedId) {
// Need to go through GObject.Object.prototype because
// nm_device_disconnect conflicts with g_signal_disconnect
GObject.Object.prototype.disconnect.call(this.device, this._stateChangedId);
this._stateChangedId = 0;
}
if (this._carrierChangedId) {
// see above for why this is needed
GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
this._carrierChangedId = 0;
}
if (this._firmwareChangedId) {
GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
this._firmwareChangedId = 0;
}
if (this._firmwareMissingId) {
GObject.Object.prototype.disconnect.call(this.device, this._firmwareMissingId);
this._firmwareMissingId = 0;
}
this._clearSection();
if (this.statusItem)
this.statusItem.destroy();
this.section.destroy();
},
deactivate: function() {
this.device.disconnect_async(null, null);
},
activate: function() {
if (this._activeConnection)
// nothing to do
return;
// pick the most recently used connection and connect to that
// or if no connections ever set, create an automatic one
if (this._connections.length > 0) {
this._client.activate_connection_async(this._connections[0].connection, this.device, null, null, null);
} else if (this._autoConnectionName) {
let connection = this._createAutomaticConnection();
if (connection)
this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
}
},
get connected() {
return this.device.state == NM.DeviceState.ACTIVATED;
},
setActiveConnection: function(activeConnection) {
if (activeConnection == this._activeConnection)
// nothing to do
return;
// remove any UI
if (this._activeConnectionItem) {
this._activeConnectionItem.destroy();
this._activeConnectionItem = null;
}
this._activeConnection = activeConnection;
this._clearSection();
this._createSection();
},
checkConnection: function(connection) {
let exists = this._findConnection(connection._uuid) != -1;
let valid = this.connectionValid(connection);
if (exists && !valid)
this.removeConnection(connection);
else if (!exists && valid)
this.addConnection(connection);
},
addConnection: function(connection) {
// record the connection
let obj = {
connection: connection,
name: connection._name,
uuid: connection._uuid,
timestamp: connection._timestamp,
};
this._connections.push(obj);
this._connections.sort(this._connectionSortFunction);
this._clearSection();
this._createSection();
},
removeConnection: function(connection) {
if (!connection._uuid) {
log('Cannot remove a connection without an UUID');
return;
}
let pos = this._findConnection(connection._uuid);
if (pos == -1) {
// this connection was never added, nothing to do here
return;
}
let obj = this._connections[pos];
if (obj.item)
obj.item.destroy();
this._connections.splice(pos, 1);
if (this._connections.length <= 1) {
// We need to show the automatic connection again
// (or in the case of NMDeviceWired, we want to hide
// the only explicit connection)
this._clearSection();
this._createSection();
}
},
connectionValid: function(connection) {
return this.device.connection_valid(connection);
},
_connectionSortFunction: function(one, two) {
if (one.timestamp == two.timestamp)
return GLib.utf8_collate(one.name, two.name);
return two.timestamp - one.timestamp;
},
setEnabled: function(enabled) {
// do nothing by default, we want to keep the connection list visible
// in the majority of cases (wired, wwan, vpn)
},
get statusLabel(){
switch(this.device.state) {
case NM.DeviceState.DISCONNECTED:
case NM.DeviceState.ACTIVATED:
return null;
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) */
return _("unmanaged");
case NM.DeviceState.DEACTIVATING:
return _("disconnecting...");
case NM.DeviceState.PREPARE:
case NM.DeviceState.CONFIG:
case NM.DeviceState.IP_CONFIG:
case NM.DeviceState.IP_CHECK:
case NM.DeviceState.SECONDARIES:
return _("connecting...");
case NM.DeviceState.NEED_AUTH:
/* Translators: this is for network connections that require some kind of key or password */
return _("authentication required");
case NM.DeviceState.UNAVAILABLE:
// This state is actually a compound of various states (generically unavailable,
// firmware missing, carrier not available), that are exposed by different properties
// (whose state may or may not updated when we receive state-changed).
if (!this._firmwareMissingId)
this._firmwareMissingId = this.device.connect('notify::firmware-missing', Lang.bind(this, this._substateChanged));
if (this.device.firmware_missing) {
/* Translators: this is for devices that require some kind of firmware or kernel
module, which is missing */
return _("firmware missing");
}
if (this.device.capabilities & NM.DeviceCapabilities.CARRIER_DETECT) {
if (!this._carrierChangedId)
this._carrierChangedId = this.device.connect('notify::carrier', Lang.bind(this, this._substateChanged));
if (!this.carrier) {
/* Translators: this is for wired network devices that are physically disconnected */
return _("cable unplugged");
}
}
/* Translators: this is for a network device that cannot be activated (for example it
is disabled by rfkill, or it has no coverage */
return _("unavailable");
case NM.DeviceState.FAILED:
return _("connection failed");
default:
log('Device state invalid, is %d'.format(this.device.state));
return 'invalid';
}
},
get controllable(){
// controllable for every state except unavailable or unmanaged
if(this.device.state === NM.DeviceState.UNAVAILABLE || this.device.state === NM.DeviceState.UNMANAGED)
return false;
return true;
},
// protected
_createAutomaticConnection: function() {
throw new TypeError('Invoking pure virtual function NMDevice.createAutomaticConnection');
},
_findConnection: function(uuid) {
for (let i = 0; i < this._connections.length; i++) {
let obj = this._connections[i];
if (obj.uuid == uuid)
return i;
}
return -1;
},
_clearSection: function() {
// Clear everything
this.section.removeAll();
this._autoConnectionItem = null;
this._activeConnectionItem = null;
this._overflowItem = null;
for (let i = 0; i < this._connections.length; i++) {
this._connections[i].item = null;
}
},
_shouldShowConnectionList: function() {
return (this.device.state >= NM.DeviceState.DISCONNECTED);
},
_createSection: function() {
if (!this._shouldShowConnectionList())
return;
if (this._activeConnection) {
this._createActiveConnectionItem();
this.section.addMenuItem(this._activeConnectionItem);
}
if (this._connections.length > 0) {
let activeOffset = this._activeConnectionItem ? 1 : 0;
for(let j = 0; j < this._connections.length; ++j) {
let obj = this._connections[j];
if (this._activeConnection &&
obj.connection == this._activeConnection.connection) {
activeOffset = 0; // we no longer need to account for this
continue;
}
obj.item = this._createConnectionItem(obj);
if (j + activeOffset >= NUM_VISIBLE_NETWORKS) {
if (!this._overflowItem) {
this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More"));
this.section.addMenuItem(this._overflowItem);
}
this._overflowItem.menu.addMenuItem(obj.item);
} else
this.section.addMenuItem(obj.item);
}
} else if (this._autoConnectionName) {
this._autoConnectionItem = new PopupMenu.PopupMenuItem(this._autoConnectionName);
this._autoConnectionItem.connect('activate', Lang.bind(this, function() {
let connection = this._createAutomaticConnection();
if (connection)
this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
}));
this.section.addMenuItem(this._autoConnectionItem);
}
},
_createConnectionItem: function(obj) {
let connection = obj.connection;
let item = new PopupMenu.PopupMenuItem(obj.name);
item.connect('activate', Lang.bind(this, function() {
this._client.activate_connection_async(connection, this.device, null, null, null);
}));
return item;
},
_createActiveConnectionItem: function() {
let title;
let active = this._activeConnection.connection;
if (active) {
title = active._name;
} else {
/* Translators: this is the indication that a connection for another logged in user is active,
and we cannot access its settings (including the name) */
title = _("Connected (private)");
}
this._activeConnectionItem = new PopupMenu.PopupMenuItem(title, { reactive: false });
this._activeConnectionItem.setShowDot(true);
this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
},
_deviceStateChanged: function(device, newstate, oldstate, reason) {
if (newstate == oldstate) {
log('device emitted state-changed without actually changing state');
return;
}
if (oldstate == NM.DeviceState.ACTIVATED) {
this.emit('network-lost');
}
if (newstate == NM.DeviceState.FAILED) {
this.emit('activation-failed', reason);
}
this._updateStatusItem();
this._clearSection();
this._createSection();
this.emit('state-changed');
},
_updateStatusItem: function() {
if (this._carrierChangedId) {
// see above for why this is needed
GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
this._carrierChangedId = 0;
}
if (this._firmwareChangedId) {
GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
this._firmwareChangedId = 0;
}
this.statusItem.setStatus(this.statusLabel);
this.statusItem.setToggleState(this.connected);
// if this device is not controllable, hide the switch
this.statusItem._switch.actor.visible = this.controllable;
},
_substateChanged: function() {
this.statusItem.setStatus(this.statusLabel);
this.statusItem._switch.actor.visible = this.controllable;
this.emit('state-changed');
},
_getDescription: function() {
let dev_product = this.device.get_product();
let dev_vendor = this.device.get_vendor();
if (!dev_product || !dev_vendor) {
if (this.device.name) {
return this.device.name;
}
else {
switch (this.device.get_device_type()) {
case NM.DeviceType.ETHERNET: return _("Ethernet");
case NM.DeviceType.WIFI: return _("Wifi");
case NM.DeviceType.MODEM: return _("Modem");
case NM.DeviceType.BT: return _("Bluetooth");
case NM.DeviceType.WIREGUARD: return _("WireGuard");
default: return "";
}
}
}
let product = Util.fixupPCIDescription(dev_product);
let vendor = Util.fixupPCIDescription(dev_vendor);
let out = '';
// Another quick hack; if all of the fixed up vendor string
// is found in product, ignore the vendor.
if (product.indexOf(vendor) == -1)
out += vendor + ' ';
out += product;
return out;
}
};
Signals.addSignalMethods(NMDevice.prototype);
function NMDeviceWired() {
this._init.apply(this, arguments);
}
NMDeviceWired.prototype = {
__proto__: NMDevice.prototype,
_init: function(client, device, connections) {
this._autoConnectionName = _("Auto Ethernet");
this.category = NMConnectionCategory.WIRED;
NMDevice.prototype._init.call(this, client, device, connections);
},
_createSection: function() {
NMDevice.prototype._createSection.call(this);
// if we have only one connection (normal or automatic)
// we hide the connection list, and use the switch to control
// the device
// we can do it here because addConnection and removeConnection
// both call _createSection at some point
if (this._connections.length <= 1)
this.section.actor.hide();
else
this.section.actor.show();
},
_createAutomaticConnection: function() {
let connection = new NM.SimpleConnection();
connection._uuid = NM.utils_uuid_generate();
connection.add_setting(new NM.SettingWired());
let setting_conn = new NM.SettingConnection({
uuid: connection._uuid,
id: this._autoConnectionName,
type: NM.SETTING_WIRED_SETTING_NAME,
autoconnect: true
});
setting_conn.add_permission('user', GLib.get_user_name(), null);
connection.add_setting(setting_conn);
return connection;
}
};
function NMDeviceModem() {
this._init.apply(this, arguments);
}
NMDeviceModem.prototype = {
__proto__: NMDevice.prototype,
_init: function(client, device, connections) {
let is_wwan = false;
this._enabled = true;
this.mobileDevice = null;
this._connectionType = 'ppp';
this._capabilities = device.current_capabilities;
// Support new ModemManager1 devices
if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0) {
try {
is_wwan = true;
this.mobileDevice = new ModemManager.BroadbandModem(device.udi, device.current_capabilities);
if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
this._connectionType = NM.SETTING_GSM_SETTING_NAME;
} else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
this._connectionType = NM.SETTING_GSM_SETTING_NAME;
} else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
}
}
catch (e){
global.logError(e);
}
} else if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
is_wwan = true;
this.mobileDevice = new ModemManager.ModemGsm(device.udi);
this._connectionType = NM.SETTING_GSM_SETTING_NAME;
} else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
is_wwan = true;
this.mobileDevice = new ModemManager.ModemCdma(device.udi);
this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
} else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
is_wwan = true;
// FIXME: support signal quality
}
if (is_wwan) {
this.category = NMConnectionCategory.WWAN;
this._autoConnectionName = _("Auto broadband");
} else {
this.category = NMConnectionCategory.WIRED;
this._autoConnectionName = _("Auto dial-up");
}
if (this.mobileDevice) {
this._operatorNameId = this.mobileDevice.connect('notify::operator-name', Lang.bind(this, function() {
if (this._operatorItem) {
let name = this.mobileDevice.operator_name;
if (name) {
this._operatorItem.label.text = name;
this._operatorItem.actor.show();
} else
this._operatorItem.actor.hide();
}
}));
this._signalQualityId = this.mobileDevice.connect('notify::signal-quality', Lang.bind(this, function() {
if (this._operatorItem) {
this._operatorItem.setIcon(this._getSignalIcon());
}
}));
}
NMDevice.prototype._init.call(this, client, device, connections);
},
setEnabled: function(enabled) {
this._enabled = enabled;
if (this.category == NMConnectionCategory.WWAN) {
if (enabled) {
// prevent "network unavailable" statuses
this.statusItem.setStatus(null);
} else
this.statusItem.setStatus(this.statusLabel);
}
NMDevice.prototype.setEnabled.call(this, enabled);
},
get connected() {
return this._enabled && this.device.state == NM.DeviceState.ACTIVATED;
},
destroy: function() {
if (this._operatorNameId) {
this.mobileDevice.disconnect(this._operatorNameId);
this._operatorNameId = 0;
}
if (this._signalQualityId) {
this.mobileDevice.disconnect(this._signalQualityId);
this._signalQualityId = 0;
}
NMDevice.prototype.destroy.call(this);
},
_getSignalIcon: function() {
return 'xsi-network-cellular-signal-' + signalToIcon(this.mobileDevice.signal_quality);
},
_createSection: function() {
if (!this._shouldShowConnectionList())
return;
if (this.mobileDevice) {
// If operator_name is null, just pass the empty string, as the item is hidden anyway
this._operatorItem = new PopupMenu.PopupImageMenuItem(this.mobileDevice.operator_name || '',
this._getSignalIcon(),
{ reactive: false });
if (!this.mobileDevice.operator_name)
this._operatorItem.actor.hide();
this.section.addMenuItem(this._operatorItem);
this._operatorItem.actor.add_style_class_name('popup-device-menu-item');
}
NMDevice.prototype._createSection.call(this);
},
_clearSection: function() {
this._operatorItem = null;
NMDevice.prototype._clearSection.call(this);
},
_createAutomaticConnection: function() {
// Mobile wizard is too complex for Cinnamon UI and
// is handled by the network panel
Util.spawn(['cinnamon-settings', 'network',
'connect-3g', this.device.get_path()]);
return null;
}
};
function NMDeviceBluetooth() {
this._init.apply(this, arguments);
}
NMDeviceBluetooth.prototype = {
__proto__: NMDevice.prototype,
_init: function(client, device, connections) {
this._autoConnectionName = this._makeConnectionName(device);
device.connect('notify::name', Lang.bind(this, this._updateAutoConnectionName));
this.category = NMConnectionCategory.WWAN;
NMDevice.prototype._init.call(this, client, device, connections);
},
_createAutomaticConnection: function() {
let connection = new NM.SimpleConnection();
connection._uuid = NM.utils_uuid_generate();
connection.add_setting(new NM.SettingBluetooth());
connection.add_setting(new NM.SettingConnection({
uuid: connection._uuid,
id: this._autoConnectionName,
type: NM.SETTING_BLUETOOTH_SETTING_NAME,
autoconnect: false
}));
return connection;
},
_makeConnectionName: function(device) {
let name = device.name;
if (name)
return _("Auto %s").format(name);
else
return _("Auto bluetooth");
},
_updateAutoConnectionName: function() {
this._autoConnectionName = this._makeConnectionName(this.device);
this._clearSection();
this._createSection();
}
};
// Not a real device, but I save a lot code this way
function NMDeviceVPN() {
this._init.apply(this, arguments);
}
NMDeviceVPN.prototype = {
__proto__: NMDevice.prototype,
_init: function(client) {
this._client = client
this._autoConnectionName = null;
this.category = NMConnectionCategory.VPN;
this._activeConnections = [];
NMDevice.prototype._init.call(this, client, null, []);
},
connectionValid: function(connection) {
return connection._type == NM.SETTING_VPN_SETTING_NAME;
},
get empty() {
return this._connections.length == 0;
},
get connected() {
return this._activeConnections.length > 0;
},
setActiveConnections: function(activeConnections) {
this._activeConnections = activeConnections || [];
this._createSection();
this.emit('active-connections-changed');
},
deactivate: function() {
for (let ac of this._activeConnections)
this._client.deactivate_connection(ac, null);
this._activeConnections = []
},
statusLabel: null,
controllable: true,
_clearSection: function() {
if (this.section && this.section.removeAll)
this.section.removeAll();
this._autoConnectionItem = null;
this._overflowItem = null;
for (let i = 0; i < this._connections.length; i++) {
if (this._connections[i].item && this._connections[i].item.destroy)
this._connections[i].item.destroy();
this._connections[i].item = null;
}
},
_updateConnectionItemView: function(item, connection, active) {
item.label.text = connection._name || _("Connected (private)");
if (active) {
item.setShowDot(true);
item.actor.add_style_class_name('popup-device-menu-item');
} else {
item.setShowDot(false);
item.actor.remove_style_class_name('popup-device-menu-item');
}
},
_createSection: function() {
for (let obj of this._connections) {
if (!obj.item) {
obj.item = new PopupMenu.PopupMenuItem(obj.name);
this._updateConnectionItemView(obj.item, obj.connection,
this._activeConnections.some(ac => ac.connection == obj.connection));
obj.item.connect('activate', Lang.bind(this, function() {
let activeConnection = this._activeConnections.find(ac => ac.connection === obj.connection);
if (activeConnection) {
this._client.deactivate_connection(activeConnection, null);
} else {
this._client.activate_connection_async(obj.connection, this.device, null, null, null);
}
}));
this.section.addMenuItem(obj.item);
} else {
this._updateConnectionItemView(obj.item, obj.connection, this._activeConnections.some(ac => ac.connection == obj.connection));
}
}
},
};
function NMDeviceWIREGUARD() {
this._init.apply(this, arguments);
}
NMDeviceWIREGUARD.prototype = {
__proto__: NMDevice.prototype,
_init: function(client, device, connections) {
// Disable autoconnections
this._autoConnectionName = null;
this._client = client;
this.category = NMConnectionCategory.WIREGUARD;
this._type = NM.SETTING_WIREGUARD_SETTING_NAME;
this._activeConnections = [];
NMDevice.prototype._init.call(this, client, null, [ ]);
// Tests:
this.category = NMConnectionCategory.WIREGUARD;
this._type = NM.SETTING_WIREGUARD_SETTING_NAME;
},
connectionValid: function(connection) {
return connection._type == NM.SETTING_WIREGUARD_SETTING_NAME;
},
get empty() {
return this._connections.length == 0;
},
get connected() {
return this._activeConnections.length > 0;
},
setActiveConnections: function(activeConnections) {
this._activeConnections = activeConnections || [];
this._createSection();
this.emit('active-connections-changed');
},
_shouldShowConnectionList: function() {
return true;
},
deactivate: function() {
for (let ac of this._activeConnections)
this._client.deactivate_connection(ac, null);
this._activeConnections = [];
},
_clearSection: function() {
if (this.section && this.section.removeAll)
this.section.removeAll();
this._autoConnectionItem = null;
this._overflowItem = null;
for (let i = 0; i < this._connections.length; i++) {
if (this._connections[i].item && this._connections[i].item.destroy)
this._connections[i].item.destroy();
this._connections[i].item = null;
}
},
/* A switch per tunnel: WireGuard connections are independent of one another,
so they are not a pick-one list. */
_createSection: function() {
for (let obj of this._connections) {
let active = this._activeConnections.some(ac => ac.connection == obj.connection);
if (!obj.item) {
obj.item = new PopupMenu.PopupSwitchMenuItem(obj.name, active);
obj.item.connect('toggled', (item, state) => {
let activeConnection = this._activeConnections.find(ac => ac.connection === obj.connection);
if (!state && activeConnection)
this._client.deactivate_connection(activeConnection, null);
else if (state && !activeConnection)
this._client.activate_connection_async(obj.connection, this.device, null, null, null);
});
this.section.addMenuItem(obj.item);
} else {
obj.item.setToggleState(active);
}
}
},
statusLabel: null,
controllable: true
};
function NMDeviceWireless() {
this._init.apply(this, arguments);
}
NMDeviceWireless.prototype = {
__proto__: NMDevice.prototype,
_init: function(client, device, connections) {
this.category = NMConnectionCategory.WIRELESS;
this._overflowItem = null;
this._networks = [ ];
this._client = client;
// breaking the layers with this, but cannot call
// this.connectionValid until I have a device
this.device = device;
let validConnections = connections.filter(Lang.bind(this, function(connection) {
return this.connectionValid(connection);
}));
let accessPoints = device.get_access_points() || [ ];
for (let i = 0; i < accessPoints.length; i++) {
// Access points are grouped by network
let ap = accessPoints[i];
if (ap.get_ssid() == null) {
// hidden access point cannot be added, we need to know
// the SSID and security details to connect
// nevertheless, the access point can acquire a SSID when
// NetworkManager connects to it (via nmcli or cinnamon-settings)
ap._notifySsidId = ap.connect('notify::ssid', Lang.bind(this, this._notifySsidCb));
continue;
}
let pos = this._findNetwork(ap);
let obj;
if (pos != -1) {
obj = this._networks[pos];
obj.accessPoints.push(ap);
} else {
obj = { ssid: ap.get_ssid(),
mode: ap.mode,
security: this._getApSecurityType(ap),
connections: [ ],
item: null,
accessPoints: [ ap ]
};
obj.ssidText = ssidToLabel(obj.ssid);
this._networks.push(obj);
}
// Check if some connection is valid for this AP
for (let j = 0; j < validConnections.length; j++) {
let connection = validConnections[j];
if (ap.connection_valid(connection) &&
obj.connections.indexOf(connection) == -1) {
obj.connections.push(connection);
}
}
}
if (this.device.active_access_point) {
let networkPos = this._findNetwork(this.device.active_access_point);
if (networkPos == -1) // the connected access point is invisible
this._activeNetwork = null;
else
this._activeNetwork = this._networks[networkPos];
} else {
this._activeNetwork = null;
}
this._networks.sort(this._networkSortFunction);
this._apChangedId = device.connect('notify::active-access-point', Lang.bind(this, this._activeApChanged));
this._apAddedId = device.connect('access-point-added', Lang.bind(this, this._accessPointAdded));
this._apRemovedId = device.connect('access-point-removed', Lang.bind(this, this._accessPointRemoved));
NMDevice.prototype._init.call(this, client, device, validConnections);
},
destroy: function() {
if (this._apChangedId) {
// see above for this HACK
GObject.Object.prototype.disconnect.call(this.device, this._apChangedId);
this._apChangedId = 0;
}
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;
}
NMDevice.prototype.destroy.call(this);
},
setEnabled: function(enabled) {
if (enabled) {
this.statusItem.actor.show();
this.section.actor.show();
} else {
this.statusItem.actor.hide();
this.section.actor.hide();
}
},
activate: function() {
if (this._activeConnection)
// nothing to do
return;
// among all visible networks, pick the last recently used connection
let best = null;
let bestApObj = null;
let bestTime = 0;
for (let i = 0; i < this._networks.length; i++) {
let apObj = this._networks[i];
for (let j = 0; j < apObj.connections.length; j++) {
let connection = apObj.connections[j];
if (connection._timestamp > bestTime) {
best = connection;
bestTime = connection._timestamp;
bestApObj = apObj;
}
}
}
if (best) {
for (let i = 0; i < bestApObj.accessPoints.length; i++) {
let ap = bestApObj.accessPoints[i];
if (ap.connection_valid(best)) {
this._client.activate_connection_async(best, this.device, ap.path, null, null);
break;
}
}
return;
}
// XXX: what else to do?
// for now, just pick a random network
// (this function is called in a corner case anyway, that is, only when
// the user toggles the switch and has more than one wireless device)
if (this._networks.length > 0) {
let connection = this._createAutomaticConnection(this._networks[0]);
let accessPoints = sortAccessPoints(this._networks[0].accessPoints);
this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
}
},
_notifySsidCb: function(accessPoint) {
if (accessPoint.get_ssid() != null) {
accessPoint.disconnect(accessPoint._notifySsidId);
accessPoint._notifySsidId = 0;
this._accessPointAdded(this.device, accessPoint);
}
},
_activeApChanged: function() {
this._activeNetwork = null;
let activeAp = this.device.active_access_point;
if (activeAp) {
let res = this._findExistingNetwork(activeAp);
if (res != null)
this._activeNetwork = this._networks[res.network];
}
// we don't refresh the view here, setActiveConnection will
},
_getApSecurityType: function(accessPoint) {
if (accessPoint._secType)
return accessPoint._secType;
let flags = accessPoint.flags;
let wpa_flags = accessPoint.wpa_flags;
let rsn_flags = accessPoint.rsn_flags;
let type;
if (rsn_flags != NM80211ApSecurityFlags.NONE) {
/* RSN check first so that WPA+WPA2 APs are treated as RSN/WPA2 */
if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
type = NMAccessPointSecurity.WPA2_ENT;
else if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
type = NMAccessPointSecurity.WPA2_PSK;
} else if (wpa_flags != NM80211ApSecurityFlags.NONE) {
if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
type = NMAccessPointSecurity.WPA_ENT;
else if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
type = NMAccessPointSecurity.WPA_PSK;
} else {
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: function(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;
return two.accessPoints[0].strength - one.accessPoints[0].strength;
/**
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: function(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: function(accessPoint) {
for (let i = 0; i < this._networks.length; i++) {
let apObj = this._networks[i];
for (let j = 0; j < apObj.accessPoints.length; j++) {
if (apObj.accessPoints[j] == accessPoint)
return { network: i, ap: j };
}
}
return null;
},
_findNetwork: function(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;
},
_accessPointAdded: function(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', Lang.bind(this, this._notifySsidCb));
return;
}
let pos = this._findNetwork(accessPoint);
let apObj;
let needsupdate = false;
if (pos != -1) {
apObj = this._networks[pos];
if (apObj.accessPoints.indexOf(accessPoint) != -1) {
log('Access point was already seen, not adding again');
return;
}
apObj.accessPoints.push(accessPoint);
if (apObj.item)
apObj.item.updateAccessPoints(apObj.accessPoints);
} else {
apObj = { ssid: accessPoint.get_ssid(),
mode: accessPoint.mode,
security: this._getApSecurityType(accessPoint),
connections: [ ],
item: null,
accessPoints: [ accessPoint ]
};
apObj.ssidText = ssidToLabel(apObj.ssid);
needsupdate = true;
}
// check if this enables new connections for this group
for (let i = 0; i < this._connections.length; i++) {
let connection = this._connections[i].connection;
if (accessPoint.connection_valid(connection) &&
apObj.connections.indexOf(connection) == -1) {
apObj.connections.push(connection);
// this potentially changes the order
needsupdate = true;
}
}
if (needsupdate) {
this._accessPointRemoved(device, accessPoint);
if (pos != -1)
this._networks.splice(pos, 1);
if (this._networks.length == 0) {
// only network in the list
this._networks.push(apObj);
this._clearSection();
this._createSection();
return;
}
// skip networks that should appear earlier
let menuPos = this._activeConnectionItem ? 1 : 0;
for (pos = 0;
pos < this._networks.length &&
this._networkSortFunction(this._networks[pos], apObj) < 0; ++pos) {
if (this._networks[pos] != this._activeNetwork)
menuPos++;
}
// (re-)add the network
this._networks.splice(pos, 0, apObj);
if (this._shouldShowConnectionList()) {
this._createNetworkItem(apObj, menuPos);
if (menuPos < NUM_VISIBLE_NETWORKS && this._networks.length > NUM_VISIBLE_NETWORKS) {
for (; menuPos < NUM_VISIBLE_NETWORKS; ++pos) {
if (this._networks[pos] != this._activeNetwork)
menuPos++;
}
let item = this._networks[pos].item;
if (item && item._apObj) {
item.destroy();
item._apObj.item = null;
this._createNetworkItem(item._apObj, NUM_VISIBLE_NETWORKS);
}
}
}
}
},
_accessPointRemoved: function(device, accessPoint) {
let res = this._findExistingNetwork(accessPoint);
if (res == null) {
return;
}
let apObj = this._networks[res.network];
apObj.accessPoints.splice(res.ap, 1);
if (apObj.accessPoints.length == 0) {
if (this._activeNetwork == apObj)
this._activeNetwork = null;
if (apObj.item)
apObj.item.destroy();
if (this._overflowItem) {
if (!apObj.isMore) {
// we removed an item in the main menu, and we have a more submenu
// we need to extract the first item in more and move it to the submenu
let item = this._overflowItem.menu.firstMenuItem;
if (item && item._apObj) {
item.destroy();
// clear the cycle, and allow the construction of the new item
item._apObj.item = null;
this._createNetworkItem(item._apObj, NUM_VISIBLE_NETWORKS-1);
} else {
log('The more... menu was existing and empty! This should not happen');
}
}
// This can happen if the removed connection is from the overflow
// menu, or if we just moved the last connection out from the menu
if (this._overflowItem.menu.numMenuItems == 0) {
this._overflowItem.destroy();
this._overflowItem = null;
}
}
this._networks.splice(res.network, 1);
} else if (apObj.item)
apObj.item.updateAccessPoints(apObj.accessPoints);
},
_createAPItem: function(connection, accessPointObj, useConnectionName) {
let item = new NMNetworkMenuItem(accessPointObj.accessPoints, useConnectionName ? connection._name : undefined);
item._connection = connection;
item.connect('activate', Lang.bind(this, function() {
let accessPoints = sortAccessPoints(accessPointObj.accessPoints);
for (let i = 0; i < accessPoints.length; i++) {
if (accessPoints[i].connection_valid(connection)) {
this._client.activate_connection_async(connection, this.device, accessPoints[i].path, null, null);
break;
}
}
}));
return item;
},
_clearSection: function() {
NMDevice.prototype._clearSection.call(this);
for (let i = 0; i < this._networks.length; i++)
this._networks[i].item = null;
this._overflowItem = null;
},
removeConnection: function(connection) {
if (!connection._uuid)
return;
let pos = this._findConnection(connection._uuid);
if (pos == -1) {
// removing connection that was never added
return;
}
this._connections.splice(pos, 1);
let anyauto = false, forceupdate = false;
for (let i = 0; i < this._networks.length; i++) {
let apObj = this._networks[i];
let connections = apObj.connections;
for (let k = 0; k < connections.length; k++) {
if (connections[k]._uuid == connection._uuid) {
// remove the connection from the access point group
connections.splice(k);
anyauto = connections.length == 0;
if (anyauto) {
// this potentially changes the sorting order
forceupdate = true;
break;
}
if (apObj.item) {
if (apObj.item instanceof PopupMenu.PopupSubMenuMenuItem) {
let items = apObj.item.menu.getMenuItems();
if (items.length == 2) {
// we need to update the connection list to convert this to a normal item
forceupdate = true;
} else {
for (let j = 0; j < items.length; j++) {
if (items[j]._connection._uuid == connection._uuid) {
items[j].destroy();
break;
}
}
}
} else {
apObj.item.destroy();
apObj.item = null;
}
}
break;
}
}
}
if (forceupdate || anyauto) {
this._networks.sort(this._networkSortFunction);
this._clearSection();
this._createSection();
}
},
addConnection: function(connection) {
// record the connection
let obj = {
connection: connection,
name: connection._name,
uuid: connection._uuid,
};
this._connections.push(obj);
// find an appropriate access point
let forceupdate = false;
for (let i = 0; i < this._networks.length; i++) {
let apObj = this._networks[i];
// Check if connection is valid for any of these access points
for (let k = 0; k < apObj.accessPoints.length; k++) {
let ap = apObj.accessPoints[k];
if (ap.connection_valid(connection)) {
apObj.connections.push(connection);
// this potentially changes the sorting order
forceupdate = true;
break;
}
}
}
if (forceupdate) {
this._networks.sort(this._networkSortFunction);
this._clearSection();
this._createSection();
}
},
_createActiveConnectionItem: function() {
if (this._activeConnection.connection) {
if (!this._activeNetwork) {
if (this.device.active_access_point) {
let networkPos = this._findNetwork(this.device.active_access_point);
if (networkPos == -1) // the connected access point is invisible
this._activeNetwork = null;
else
this._activeNetwork = this._networks[networkPos];
} else {
this._activeNetwork = null;
}
}
if (this._activeNetwork)
this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined, { reactive: false });
else
this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(this._activeConnection.connection._name, 'xsi-network-wireless-connected', { reactive: false });
} else {
// We cannot read the connection (due to ACL, or API incompatibility), but we still show signal if we have it
if (this._activeNetwork)
this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined,
{ reactive: false });
else
this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(_("Connected (private)"),
'xsi-network-wireless-connected',
{ reactive: false });
}
this._activeConnectionItem.setShowDot(true);
this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
},
_createAutomaticConnection: function(apObj) {
let name;
let ssid = NM.utils_ssid_to_utf8(apObj.ssid.get_data());
if (ssid) {
/* Translators: this the automatic wireless connection name (including the network name) */
name = _("Auto %s").format(ssid);
} else
name = _("Auto wireless");
let connection = new NM.SimpleConnection();
connection.add_setting(new NM.SettingWireless());
let setting_conn = new NM.SettingConnection({
id: name,
autoconnect: true, // NetworkManager will know to ignore this if appropriate
uuid: NM.utils_uuid_generate(),
type: NM.SETTING_WIRELESS_SETTING_NAME
});
if (default_to_private_connections(this._client)) {
setting_conn.add_permission('user', GLib.get_user_name(), null);
if (apObj.security == NMAccessPointSecurity.WPA2_PSK ||
apObj.security == NMAccessPointSecurity.WPA_PSK) {
connection.add_setting(new NM.SettingWirelessSecurity({
psk_flags: NM.SettingSecretFlags.AGENT_OWNED
}));
}
if (apObj.security == NMAccessPointSecurity.WEP) {
connection.add_setting(new NM.SettingWirelessSecurity({
wep_key_flags: NM.SettingSecretFlags.AGENT_OWNED
}));
}
}
connection.add_setting(setting_conn);
return connection;
},
_createNetworkItem: function(apObj, position) {
if(!apObj.accessPoints || apObj.accessPoints.length == 0) {
// this should not happen, but I have no idea why it happens
return;
}
if(apObj.connections.length > 0) {
if (apObj.connections.length == 1) {
apObj.item = this._createAPItem(apObj.connections[0], apObj, false);
} else {
let title = apObj.ssidText;
apObj.item = new PopupMenu.PopupSubMenuMenuItem(title);
for (let i = 0; i < apObj.connections.length; i++)
apObj.item.menu.addMenuItem(this._createAPItem(apObj.connections[i], apObj, true));
}
} else {
apObj.item = new NMNetworkMenuItem(apObj.accessPoints);
apObj.item.connect('activate', Lang.bind(this, function() {
let accessPoints = sortAccessPoints(apObj.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 cinnamon-settings
Util.spawn(['cinnamon-settings', 'network', 'connect-8021x-wifi',
this.device.get_path(), accessPoints[0].path]);
} else {
let connection = this._createAutomaticConnection(apObj);
this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
}
}));
}
apObj.item._apObj = apObj;
if (position < NUM_VISIBLE_NETWORKS) {
apObj.isMore = false;
this.section.addMenuItem(apObj.item, position);
} else {
if (!this._overflowItem) {
this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More"));
this.section.addMenuItem(this._overflowItem);
}
this._overflowItem.menu.addMenuItem(apObj.item, position - NUM_VISIBLE_NETWORKS);
apObj.isMore = true;
}
},
_createSection: function() {
if (!this._shouldShowConnectionList())
return;
if(this._activeConnection) {
this._createActiveConnectionItem();
this.section.addMenuItem(this._activeConnectionItem);
}
let activeOffset = this._activeConnectionItem ? 1 : 0;
for(let j = 0; j < this._networks.length; j++) {
let apObj = this._networks[j];
if (apObj == this._activeNetwork) {
activeOffset = 0; // we no longer need to account for this
continue;
}
this._createNetworkItem(apObj, j + activeOffset);
}
},
};
function NMMessageTraySource() {
this._init();
}
NMMessageTraySource.prototype = {
__proto__: MessageTray.Source.prototype,
_init: function() {
MessageTray.Source.prototype._init.call(this, _("Network Manager"));
let icon = new St.Icon({ icon_name: 'xsi-network-transmit-receive',
icon_type: St.IconType.SYMBOLIC,
icon_size: this.ICON_SIZE
});
this._setSummaryIcon(icon);
}
};
function CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id) {
this._init(metadata, orientation, panel_height, instance_id);
}
CinnamonNetworkApplet.prototype = {
__proto__: Applet.IconApplet.prototype,
_init: function(metadata, orientation, panel_height, instance_id) {
Applet.IconApplet.prototype._init.call(this, orientation, panel_height, instance_id);
try {
this.metadata = metadata;
Main.systrayManager.registerTrayIconReplacement("network", metadata.uuid);
Main.systrayManager.registerTrayIconReplacement("nm-applet", metadata.uuid);
this.menuManager = new PopupMenu.PopupMenuManager(this);
this.menu = new Applet.AppletPopupMenu(this, orientation);
this.menuManager.addMenu(this.menu);
this._currentIconName = undefined;
this._setIcon('xsi-network-offline');
this.settings = new Settings.AppletSettings(this, metadata.uuid, this.instance_id);
this.settings.bind("keyOpen", "keyOpen", this._setKeybinding);
this._setKeybinding();
NM.Client.new_async(null, Lang.bind(this, this._clientGot));
}
catch (e) {
global.logError(e);
}
},
_setKeybinding() {
Main.keybindingManager.addXletHotKey(this, "network-open", this.keyOpen, Lang.bind(this, this._openMenu));
},
_clientGot: function(obj, result) {
try {
this._client = NM.Client.new_finish(result);
this._v1_28_0 = Util.version_exceeds(this._client.get_version(), "1.28.0");
this._nm_proxy = null;
if (!this._v1_28_0) {
new NMDBus(Lang.bind(this, function(proxy, error) {
this._nm_proxy = proxy;
}));
}
this._statusSection = new PopupMenu.PopupMenuSection();
this._statusItem = new PopupMenu.PopupMenuItem('', { style_class: 'popup-inactive-menu-item', reactive: false });
this._statusSection.addMenuItem(this._statusItem);
this._statusSection.addAction(_("Enable networking"), Lang.bind(this, function() {
this._client.networking_enabled = true;
}));
this._statusSection.actor.hide();
this.menu.addMenuItem(this._statusSection);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._devices = { };
this._devices.wired = {
section: new PopupMenu.PopupMenuSection(),
devices: [ ],
item: new NMWiredSectionTitleMenuItem(_("Wired"))
};
this._devices.wired.section.addMenuItem(this._devices.wired.item);
this._devices.wired.section.actor.hide();
this.menu.addMenuItem(this._devices.wired.section);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._devices.wireless = {
section: new PopupMenu.PopupMenuSection(),
devices: [ ],
item: this._makeToggleItem('wireless', _("Wireless"))
};
this._devices.wireless.section.addMenuItem(this._devices.wireless.item);
this._devices.wireless.section.actor.hide();
this.menu.addMenuItem(this._devices.wireless.section);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._devices.wwan = {
section: new PopupMenu.PopupMenuSection(),
devices: [ ],
item: this._makeToggleItem('wwan', _("Mobile broadband"))
};
this._devices.wwan.section.addMenuItem(this._devices.wwan.item);
this._devices.wwan.section.actor.hide();
this.menu.addMenuItem(this._devices.wwan.section);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._devices.vpn = {
section: new PopupMenu.PopupMenuSection(),
device: new NMDeviceVPN(this._client),
item: new NMWiredSectionTitleMenuItem(_("VPN Connections"))
};
this._devices.vpn.device.connect('active-connections-changed', Lang.bind(this, function() {
this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
}));
this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
this._devices.vpn.section.addMenuItem(this._devices.vpn.item);
this._devices.vpn.section.addMenuItem(this._devices.vpn.device.section);
this._devices.vpn.section.actor.hide();
this.menu.addMenuItem(this._devices.vpn.section);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._devices.wireguard = {
section: new PopupMenu.PopupMenuSection(),
device: new NMDeviceWIREGUARD(this._client),
item: new NMWiredSectionTitleMenuItem(_("WIREGUARD Connections"))
};
this._devices.wireguard.device.connect('active-connections-changed', () => {
this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
});
this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
this._devices.wireguard.section.addMenuItem(this._devices.wireguard.item);
this._devices.wireguard.section.addMenuItem(this._devices.wireguard.device.section);
this._devices.wireguard.section.actor.hide();
this.menu.addMenuItem(this._devices.wireguard.section);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addSettingsAction(_("Network Settings"), 'network');
this.menu.addAction(_("Network Connections"), Lang.bind(this, function() {
Util.spawnCommandLine("nm-connection-editor");
}));
this.menu.connect("open-state-changed", Lang.bind(this, this._onMenuOpenStateChanged));
this._periodicTimeoutId = 0;
this._updateFrequencySeconds = 0;
this._checkingConnectivity = false;
this._lastConnectivityState = NM.ConnectivityState.UNKNOWN;
this._activeConnections = [ ];
this._connections = [ ];
this._mainConnection = null;
// 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_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS;
this._ctypes[NM.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED;
this._ctypes[NM.SETTING_PPPOE_SETTING_NAME] = NMConnectionCategory.WIRED;
this._ctypes[NM.SETTING_PPP_SETTING_NAME] = NMConnectionCategory.WIRED;
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._ctypes[NM.SETTING_WIREGUARD_SETTING_NAME] = NMConnectionCategory.WIREGUARD;
this._readConnections();
this._readDevices();
this._syncNMState();
this._client.connect('notify::nm-running', Lang.bind(this, this._syncNMState));
this._client.connect('notify::networking-enabled', Lang.bind(this, this._syncNMState));
this._client.connect('notify::state', Lang.bind(this, this._syncNMState));
this._client.connect('notify::active-connections', Lang.bind(this, this._updateIcon));
this._client.connect('device-added', Lang.bind(this, this._deviceAdded));
this._client.connect('device-removed', Lang.bind(this, this._deviceRemoved));
this._client.connect('connection-added', Lang.bind(this, this._connectionAdded));
this._client.connect('connection-removed', Lang.bind(this, this._connectionRemoved));
this._inited = true;
this._periodicUpdateIcon();
}
catch (e) {
global.logError(e);
}
},
_setIcon: function(name) {
if (this._currentIconName !== name) {
this.set_applet_icon_symbolic_name(name);
this._currentIconName = name;
}
},
on_applet_clicked: function(event) {
this.menu.toggle();
},
_openMenu: function () {
this.menu.toggle();
},
_ensureSource: function() {
if (!this._source) {
this._source = new NMMessageTraySource();
this._source.connect('destroy', Lang.bind(this, function() {
this._source = null;
}));
if (Main.messageTray) Main.messageTray.add(this._source);
}
},
_makeToggleItem: function(type, title) {
let item = new NMWirelessSectionTitleMenuItem(this._client, type, title);
item.connect('enabled-changed', Lang.bind(this, function(item, enabled) {
let devices = this._devices[type].devices;
devices.forEach(function(dev) {
dev.setEnabled(enabled);
});
this._syncSectionTitle(type);
}));
return item;
},
_syncSectionTitle: function(category) {
let devices = this._devices[category].devices;
let vis_devices = [];
let item = this._devices[category].item;
let section = this._devices[category].section;
/* We don't want unmanaged entries to clutter the list, but we do need to
* keep them around in case they become manageable and need to be made visible
* later (some valid wifi hardware starts out initially unmanaged, for instance.)
* We'll only count managed devices for evaluating what entries to display, and
* when to show or hide section titles. */
if (devices) {
for (let i = 0; i < devices.length; i++) {
if (devices[i].device.state == NM.DeviceState.UNMANAGED) {
continue;
}
vis_devices.push(devices[i]);
}
}
if (vis_devices.length == 0) {
section.actor.hide();
} else {
section.actor.show();
if (vis_devices.length == 1) {
devices.forEach(function(dev) {
dev.statusItem.actor.hide();
});
let dev = vis_devices[0];
item.updateForDevice(dev);
} else {
devices.forEach(function(dev) {
dev.statusItem.actor.visible = (dev.device.state != NM.DeviceState.UNMANAGED);
});
// remove status text from the section title item
item.updateForDevice(null);
}
}
},
_readDevices: function() {
let devices = this._client.get_devices() || [ ];
for (let i = 0; i < devices.length; ++i) {
this._deviceAdded(this._client, devices[i]);
}
},
_notifyForDevice: function(device, iconName, title, text, urgency) {
if (device._notification)
device._notification.destroy();
/* must call after destroying previous notification,
or this._source will be cleared */
this._ensureSource();
let icon = new St.Icon({ icon_name: iconName,
icon_type: St.IconType.SYMBOLIC,
icon_size: this._source.ICON_SIZE
});
device._notification = new MessageTray.Notification(this._source, title, text,
{ icon: icon });
device._notification.setUrgency(urgency);
device._notification.setTransient(true);
device._notification.connect('destroy', function() {
device._notification = null;
});
this._source.notify(device._notification);
},
_deviceAdded: function(client, device) {
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, this._connections);
wrapper._activationFailedId = wrapper.connect('activation-failed', Lang.bind(this, function(device, reason) {
// XXX: nm-applet has no special text depending on reason
// but I'm not sure of this generic message
this._notifyForDevice(device, 'network-error',
_("Connection failed"),
_("Activation of network connection failed"),
MessageTray.Urgency.HIGH);
}));
wrapper._deviceStateChangedId = wrapper.connect('state-changed', Lang.bind(this, function(dev) {
this._syncSectionTitle(dev.category);
}));
wrapper._destroyId = wrapper.connect('destroy', function(wrapper) {
wrapper.disconnect(wrapper._activationFailedId);
wrapper.disconnect(wrapper._deviceStateChangedId);
wrapper.disconnect(wrapper._destroyId);
});
let section = this._devices[wrapper.category].section;
let devices = this._devices[wrapper.category].devices;
section.addMenuItem(wrapper.section, 1);
section.addMenuItem(wrapper.statusItem, 1);
devices.push(wrapper);
this._syncSectionTitle(wrapper.category);
}
},
_deviceRemoved: function(client, device) {
if (!device._delegate) {
return;
}
let wrapper = device._delegate;
wrapper.destroy();
let devices = this._devices[wrapper.category].devices;
let pos = devices.indexOf(wrapper);
devices.splice(pos, 1);
this._syncSectionTitle(wrapper.category);
},
_syncActiveConnections: function() {
let closedConnections = [];
let newActiveConnections = this._client.get_active_connections() || [];
for (let a of this._activeConnections) {
if (!newActiveConnections.includes(a))
closedConnections.push(a);
}
for (let active of closedConnections) {
if (active._primaryDevice) {
if (active._type == NM.SETTING_VPN_SETTING_NAME)
this._devices.vpn.device.setActiveConnections([]);
else if (active._type == NM.SETTING_WIREGUARD_SETTING_NAME)
this._devices.wireguard.device.setActiveConnections([]);
else
active._primaryDevice.setActiveConnection(null);
active._primaryDevice = null;
}
if (active._inited) {
active.disconnect(active._notifyStateId);
active._inited = false;
}
}
this._activeConnections = newActiveConnections;
this._mainConnection = null;
let activating = null;
let activated = null;
let default_ip4 = null;
let default_ip6 = null;
let vpnConnections = [];
let wireguardConnections = [];
for (let a of this._activeConnections) {
if (!a._inited) {
a._notifyStateId = a.connect('notify::state', Lang.bind(this, this._notifyActivated));
a._inited = true;
}
if (!a._type) {
a._type = a.connection._type;
a._section = this._ctypes[a._type];
if (a._errorLogged) {
log('network applet: Found connection for active');
a._errorLogged = false;
}
}
if (!a._section){
// Do not take connections which section is undefined into account
// For instance, in Mint 18, when the "vpn" is "activated", we sometimes see a "tun" act as the default ipv4 connection.
continue;
}
if (a.state == NM.ActiveConnectionState.ACTIVATED) {
if (!default_ip4) {
// We didn't find the default IPV4 device yet..
if (!default_ip6) {
// We didn't find the default IPV6 device either so consider this one.
activated = a;
}
else {
// We already found the default IPV6 device, so only use this one if it's the default IPV4 device
if (a['default']) {
activated = a;
}
}
}
}
if (a.state == NM.ActiveConnectionState.ACTIVATING) {
activating = a;
}
if (a['default']) {
default_ip4 = a;
}
if (a.default6) {
default_ip6 = a;
}
if (!a._primaryDevice) {
if (a._type != NM.SETTING_VPN_SETTING_NAME && a._type != NM.SETTING_WIREGUARD_SETTING_NAME) {
// find a good device to be considered primary
a._primaryDevice = null;
let devices = a.get_devices() || [ ];
for (let j = 0; j < devices.length; j++) {
let d = devices[j];
if (d._delegate) {
a._primaryDevice = d._delegate;
break;
}
}
} else {
if (a._type == NM.SETTING_VPN_SETTING_NAME)
a._primaryDevice = this._devices.vpn.device;
else {
a._primaryDevice = this._devices.wireguard.device;
}
}
if (a.state == NM.ActiveConnectionState.ACTIVATED &&
a._primaryDevice && a._primaryDevice._notification) {
a._primaryDevice._notification.destroy();
a._primaryDevice._notification = null;
}
}
if (a._primaryDevice) {
if (a._type == NM.SETTING_VPN_SETTING_NAME)
vpnConnections.push(a);
else if (a._type == NM.SETTING_WIREGUARD_SETTING_NAME)
wireguardConnections.push(a);
else
a._primaryDevice.setActiveConnection(a);
}
}
if (this._devices.vpn && this._devices.vpn.device)
this._devices.vpn.device.setActiveConnections(vpnConnections);
if (this._devices.wireguard && this._devices.wireguard.device)
this._devices.wireguard.device.setActiveConnections(wireguardConnections);
this._mainConnection = activated || activating || default_ip4 || default_ip6 || null;
},
_notifyActivated: function(activeConnection) {
if (activeConnection.state == NM.ActiveConnectionState.ACTIVATED &&
activeConnection._primaryDevice && activeConnection._primaryDevice._notification) {
activeConnection._primaryDevice._notification.destroy();
activeConnection._primaryDevice._notification = null;
}
this._updateIcon();
},
_readConnections: function() {
let connections = this._client.get_connections();
for (let i = 0; i < connections.length; i++) {
let connection = connections[i];
if (connection._uuid) {
// connection was already seen (for example because NetworkManager was restarted)
continue;
}
connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));
this._updateConnection(connection);
this._connections.push(connection);
}
},
_connectionAdded: function(client, connection) {
if (connection._uuid) {
// connection was already seen
return;
}
connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));
this._updateConnection(connection);
this._connections.push(connection);
this._updateIcon();
},
_connectionRemoved: function(client, connection) {
let pos = this._connections.indexOf(connection);
if (pos != -1)
this._connections.splice(pos, 1);
let section = connection._section;
if (section == NMConnectionCategory.VPN) {
this._devices.vpn.device.removeConnection(connection);
if (this._devices.vpn.device.empty)
this._devices.vpn.section.actor.hide();
} else if (section == NMConnectionCategory.WIREGUARD) {
this._devices.wireguard.device.removeConnection(connection);
if (this._devices.wireguard.device.empty)
this._devices.wireguard.section.actor.hide();
} else if (section != NMConnectionCategory.INVALID) {
let devices = this._devices[section].devices;
for (let i = 0; i < devices.length; i++)
devices[i].removeConnection(connection);
}
connection._uuid = null;
connection.disconnect(connection._updatedId);
},
_updateConnection: function(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;
connection._name = connectionSettings.id;
connection._uuid = connectionSettings.uuid;
connection._timestamp = connectionSettings.timestamp;
let section = connection._section;
if (connection._section == NMConnectionCategory.INVALID)
return;
if (section == NMConnectionCategory.VPN) {
this._devices.vpn.device.checkConnection(connection);
this._devices.vpn.section.actor.show();
} else if (section == NMConnectionCategory.WIREGUARD) {
this._devices.wireguard.device.checkConnection(connection);
this._devices.wireguard.section.actor.show();
} else {
let devices = this._devices[section].devices;
for (let i = 0; i < devices.length; i++) {
devices[i].checkConnection(connection);
}
}
},
_hideDevices: function() {
this._devicesHidden = true;
for (let category in this._devices)
this._devices[category].section.actor.hide();
},
_showNormal: function() {
if (!this._devicesHidden) // nothing to do
return;
this._devicesHidden = false;
this._statusSection.actor.hide();
this._syncSectionTitle('wired');
this._syncSectionTitle('wireless');
this._syncSectionTitle('wwan');
if (!this._devices.vpn.device.empty)
this._devices.vpn.section.actor.show();
if (!this._devices.wireguard.device.empty)
this._devices.wireguard.section.actor.show();
},
_syncNMState: function() {
if (!this._client.nm_running) {
log('NetworkManager is not running, hiding...');
this.menu.close();
this.actor.hide();
return;
} else
this.actor.show();
if (!this._client.networking_enabled) {
this._setIcon('xsi-network-offline');
this._hideDevices();
this._statusItem.label.text = _("Networking is disabled");
this._statusSection.actor.show();
return;
}
this._showNormal();
this._updateIcon();
},
_updateIcon: function() {
let new_delay = DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS;
try {
this._syncActiveConnections();
let mc = this._mainConnection;
if (!mc) {
this._setIcon('xsi-network-offline');
this.set_applet_tooltip(_("No connection"));
} else if (mc.state == NM.ActiveConnectionState.ACTIVATING) {
new_delay = FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
switch (mc._section) {
case NMConnectionCategory.WWAN:
this._setIcon('xsi-network-cellular-acquiring');
this.set_applet_tooltip(_("Connecting to the cellular network..."));
break;
case NMConnectionCategory.WIRELESS:
this._setIcon('xsi-network-wireless-acquiring');
this.set_applet_tooltip(_("Connecting to the wireless network..."));
break;
case NMConnectionCategory.WIRED:
this._setIcon('xsi-network-wired-acquiring');
this.set_applet_tooltip(_("Connecting to the wired network..."));
break;
case NMConnectionCategory.VPN:
this._setIcon('xsi-network-vpn-acquiring');
this.set_applet_tooltip(_("Connecting to the VPN..."));
break;
case NMConnectionCategory.WIREGUARD:
this._setIcon('xsi-network-vpn-acquiring');
this.set_applet_tooltip(_("Connecting to WIREGUARD..."));
break;
default:
// fallback to a generic connected icon
// (it could be a private connection of some other user)
this._setIcon('xsi-network-wired-acquiring');
this.set_applet_tooltip(_("Connecting to the network..."));
}
} else {
let dev;
let limited_conn = false;
// let limited_conn = this._correctStateForTunnel(this._client.get_connectivity()) !== NM.ConnectivityState.FULL;
switch (mc._section) {
case NMConnectionCategory.WIRELESS:
dev = mc._primaryDevice;
if (dev) {
let ap = dev.device.active_access_point;
let mode = dev.device.mode;
if (!ap) {
if (mode != NM80211Mode.ADHOC) {
log('An active wireless connection, in infrastructure mode, involves no access point?');
break;
}
this._setIcon('xsi-network-wireless-connected');
this.set_applet_tooltip(_("Connected to the wireless network"));
} else {
if (limited_conn) {
this._setIcon('xsi-network-wireless-no-route');
this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " " + _("(Limited connectivity)"));
} else {
this._setIcon('xsi-network-wireless-signal-' + signalToIcon(ap.strength));
this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " ("+ ap.strength +"%)");
}
}
} else {
log('Active connection with no primary device?');
}
break;
case NMConnectionCategory.WIRED:
if (limited_conn) {
this._setIcon('xsi-network-wired-no-route');
this.set_applet_tooltip(_("Connected to the wired network") + " " + _("(Limited connectivity)"));
} else {
this._setIcon('xsi-network-wired');
this.set_applet_tooltip(_("Connected to the wired network"));
}
break;
case NMConnectionCategory.WWAN:
dev = mc._primaryDevice;
if (!dev) {
log('Active connection with no primary device?');
break;
}
if (!dev.mobileDevice) {
// this can happen for bluetooth in PAN mode
this._setIcon('xsi-network-cellular-connected');
this.set_applet_tooltip(_("Connected to the cellular network"));
break;
}
if (limited_conn) {
this._setIcon('xsi-network-cellular-no-route');
this.set_applet_tooltip(_("Connected to the cellular network") + " " + _("(Limited connectivity)"));
} else {
this._setIcon('xsi-network-cellular-signal-' + signalToIcon(dev.mobileDevice.signal_quality));
this.set_applet_tooltip(_("Connected to the cellular network"));
}
break;
case NMConnectionCategory.VPN:
// Should we indicate limited connectivity for VPNs like we do above? What if the connection is to
// a local machine? Need to test.
this._setIcon('xsi-network-vpn');
this.set_applet_tooltip(_("Connected to the VPN"));
break;
case NMConnectionCategory.WIREGUARD:
// Should we indicate limited connectivity for WIREGUARDs like we do above? What if the connection is to
// a local machine? Need to test.
this._setIcon('xsi-network-vpn');
this.set_applet_tooltip(_("Connected to WIREGUARD"));
break;
default:
// fallback to a generic connected icon
// (it could be a private connection of some other user)
this._setIcon('xsi-network-wired');
this.set_applet_tooltip(_("Connected to the network"));
break;
}
}
if (this._devices.wireguard.item && this._devices.wireguard.item._switch.state) {
this._setIcon('xsi-network-vpn');
this.set_applet_tooltip(_("Connected to WIREGUARD"));
}
for (let i = 0; i < this._activeConnections.length; i++) {
const a = this._activeConnections[i];
if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATING) {
this._setIcon('xsi-network-vpn-acquiring');
this.set_applet_tooltip(_("Connecting to the VPN..."));
break;
}
else if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATED) {
let iconName = 'xsi-network-vpn';
if (mc._section == NMConnectionCategory.WIRELESS) {
const dev = mc._primaryDevice;
const ap = dev ? dev.device.active_access_point : null;
if (ap)
iconName = 'xsi-network-wireless-signal-' + signalToIcon(ap.strength) + '-secure';
}
this._setIcon(iconName);
this.set_applet_tooltip(_("Connected to the VPN"));
break;
}
}
}
catch (e) {
global.logError(e);
}
if (this.menu.isOpen) {
return FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
}
return new_delay;
},
_periodicUpdateIcon: function() {
let new_delay = this._updateIcon();
this._lastConnectivityState = this._client.get_connectivity();
// TODO: This can be unreliable with multiple interfaces, and libnm
// can be flaky with its connectivity state - on one machine of mine,
// I observed regular switching between full and limited connectivity,
// when no noticeable change in my actual ability to reach the internet.
// We might re-implement to be independent of libnm. For now just disable it.
//
// if (!this._checkingConnectivity) {
// // https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/476
// if (this._v1_28_0) {
// this._checkingConnectivity = true;
// this._client.check_connectivity_async(null, Lang.bind(this, this._connectivityCheckCallback));
// } else
// if (this._nm_proxy != null) {
// this._checkingConnectivity = true;
// this._nm_proxy.CheckConnectivityRemote(Lang.bind(this, this._proxyConnectivityCheckCallback));
// }
// }
if (this._updateFrequencySeconds != new_delay) {
this._restartPeriodicUpdateTimer(new_delay);
} else {
return GLib.SOURCE_CONTINUE;
}
return GLib.SOURCE_REMOVE;
},
_correctStateForTunnel(state) {
if (state !== NM.ConnectivityState.LIMITED) {
return state;
}
// if our primary connection state is 'limited', check if there's a tunnel
// connection active, and assume 'full' if so - this prevents reporting limited
// connectivity when something like PIA vpn is active. This could end up reporting
// a full connection when there isn't one, depending on what the tunnel is for,
// but NM doesn't seem to provide any way to discern a relationship between a virtual
// device (tunnel) and the physical device in use. The physical device is always returned
// as the primary.
if (this._activeConnections.some(con => con.get_connection_type() === "tun")) {
return NM.ConnectivityState.FULL;
}
if (this._activeConnections.some(con => con.get_connection_type() === "wireguard")) {
return NM.ConnectivityState.FULL;
}
return state;
},
_proxyConnectivityCheckCallback(new_state) {
let state = this._correctStateForTunnel(new_state[0]);
if (state !== this._lastConnectivityState) {
this._updateIcon();
}
this._checkingConnectivity = false;
},
_connectivityCheckCallback(source, result) {
let new_state = NM.ConnectivityState.UNKNOWN;
try {
new_state = source.check_connectivity_finish(result);
} catch (e) {
log(e);
}
let state = this._correctStateForTunnel(new_state);
if (state !== this._lastConnectivityState) {
this._updateIcon();
}
this._checkingConnectivity = false;
},
_restartPeriodicUpdateTimer: function(new_delay) {
if (this._periodicTimeoutId > 0) {
Mainloop.source_remove(this._periodicTimeoutId);
}
this._updateFrequencySeconds = new_delay;
this._periodicTimeoutId = Mainloop.timeout_add_seconds(this._updateFrequencySeconds, Lang.bind(this, this._periodicUpdateIcon));
},
_rescanAccessPoints: function() {
try {
let devices = this._devices.wireless.devices;
for (let i = 0; i < devices.length; i++) {
if (devices[i]._client.wireless_get_enabled()) {
devices[i].device.request_scan(null);
}
}
} catch (gerror) {
if (gerror.code == NM.DeviceError.NOTALLOWED) {
// NM only allows a rescan every 10 seconds
return;
} else {
log(gerror);
}
}
},
_onMenuOpenStateChanged: function(popup, open) {
this._periodicUpdateIcon();
if (open) {
this._rescanAccessPoints();
}
},
on_applet_removed_from_panel: function() {
Main.systrayManager.unregisterTrayIconReplacement(this.metadata.uuid);
Main.keybindingManager.removeXletHotKey(this, "network-open");
if (this._periodicTimeoutId){
Mainloop.source_remove(this._periodicTimeoutId);
}
},
};
function main(metadata, orientation, panel_height, instance_id) {
return new CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id);
}