gnome-control-center/panels/network/cc-wifi-connection-list.c
2020-07-20 10:38:09 +03:00

759 lines
24 KiB
C

/*
* Copyright © 2018 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
#include "cc-wifi-connection-list.h"
#include "cc-wifi-connection-row.h"
struct _CcWifiConnectionList
{
GtkListBox parent_instance;
guint freeze_count;
gboolean updating;
gboolean checkable;
gboolean hide_unavailable;
gboolean show_aps;
NMClient *client;
NMDeviceWifi *device;
NMConnection *last_active;
GPtrArray *connections;
GPtrArray *connections_row;
/* AP SSID cache stores the APs SSID used for assigning it to a row.
* This is necessary to efficiently remove it when its SSID changes.
*
* Note that we only group APs that cannot be assigned to a connection
* by the SSID. In principle this is wrong, because other attributes may
* be different rendering them separate networks.
* In practice this will almost never happen, and if it does, we just
* show and select the strongest AP.
*/
GHashTable *ap_ssid_cache;
GHashTable *ssid_to_row;
};
static void on_device_ap_added_cb (CcWifiConnectionList *self,
NMAccessPoint *ap,
NMDeviceWifi *device);
static void on_device_ap_removed_cb (CcWifiConnectionList *self,
NMAccessPoint *ap,
NMDeviceWifi *device);
static void on_row_configured_cb (CcWifiConnectionList *self,
CcWifiConnectionRow *row);
G_DEFINE_TYPE (CcWifiConnectionList, cc_wifi_connection_list, GTK_TYPE_LIST_BOX)
enum
{
PROP_0,
PROP_CHECKABLE,
PROP_HIDE_UNAVAILABLE,
PROP_SHOW_APS,
PROP_CLIENT,
PROP_DEVICE,
PROP_LAST
};
static GParamSpec *props [PROP_LAST];
static GBytes*
new_hashable_ssid (GBytes *ssid)
{
GBytes *res;
const guint8 *data;
gsize size;
/* This is what nm_utils_same_ssid does, but returning it so that we can
* use the result in other ways (i.e. hash table lookups). */
data = g_bytes_get_data ((GBytes*) ssid, &size);
if (data[size-1] == '\0')
size -= 1;
res = g_bytes_new (data, size);
return res;
}
static gboolean
connection_ignored (NMConnection *connection)
{
NMSettingWireless *sw;
/* Ignore AP and adhoc modes (i.e. accept infrastructure or empty) */
sw = nm_connection_get_setting_wireless (connection);
if (!sw)
return TRUE;
if (g_strcmp0 (nm_setting_wireless_get_mode (sw), "adhoc") == 0 ||
g_strcmp0 (nm_setting_wireless_get_mode (sw), "ap") == 0)
{
return TRUE;
}
return FALSE;
}
static CcWifiConnectionRow*
cc_wifi_connection_list_row_add (CcWifiConnectionList *self,
NMConnection *connection,
NMAccessPoint *ap)
{
CcWifiConnectionRow *res;
g_autoptr(GPtrArray) aps = NULL;
if (ap)
{
aps = g_ptr_array_new ();
g_ptr_array_add (aps, ap);
}
res = cc_wifi_connection_row_new (self->device,
connection,
aps,
self->checkable);
gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (res));
gtk_widget_show (GTK_WIDGET (res));
g_signal_connect_object (res, "configure", G_CALLBACK (on_row_configured_cb), self, G_CONNECT_SWAPPED);
return res;
}
static void
clear_widget (CcWifiConnectionList *self)
{
const GPtrArray *aps;
GHashTableIter iter;
CcWifiConnectionRow *row;
gint i;
/* Clear everything; disconnect all AP signals first */
aps = nm_device_wifi_get_access_points (self->device);
for (i = 0; i < aps->len; i++)
g_signal_handlers_disconnect_by_data (g_ptr_array_index (aps, i), self);
/* Remove all AP only rows */
g_hash_table_iter_init (&iter, self->ssid_to_row);
while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &row))
{
g_hash_table_iter_remove (&iter);
gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (row));
}
/* Remove all connection rows */
for (i = 0; i < self->connections_row->len; i++)
{
if (!g_ptr_array_index (self->connections_row, i))
continue;
row = g_ptr_array_index (self->connections_row, i);
g_ptr_array_index (self->connections_row, i) = NULL;
gtk_container_remove (GTK_CONTAINER (self),
GTK_WIDGET (row));
}
/* Reset the internal state */
g_ptr_array_set_size (self->connections, 0);
g_ptr_array_set_size (self->connections_row, 0);
g_hash_table_remove_all (self->ssid_to_row);
g_hash_table_remove_all (self->ap_ssid_cache);
}
static void
update_connections (CcWifiConnectionList *self)
{
const GPtrArray *aps;
const GPtrArray *acs_client;
g_autoptr(GPtrArray) acs = NULL;
NMActiveConnection *ac;
NMConnection *ac_con = NULL;
gint i;
/* We don't want full UI rebuilds during some UI interactions, so allow freezing the list. */
if (self->freeze_count > 0)
return;
/* Prevent recursion (maybe move this into an idle handler instead?) */
if (self->updating)
return;
self->updating = TRUE;
clear_widget (self);
/* Copy the new connections; also create a row if we show unavailable
* connections */
acs_client = nm_client_get_connections (self->client);
acs = g_ptr_array_new_full (acs_client->len + 1, NULL);
for (i = 0; i < acs_client->len; i++)
g_ptr_array_add (acs, g_ptr_array_index (acs_client, i));
ac = nm_device_get_active_connection (NM_DEVICE (self->device));
if (ac)
ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac));
if (ac_con && !g_ptr_array_find (acs, ac_con, NULL))
{
g_debug ("Adding remote connection for active connection");
g_ptr_array_add (acs, g_object_ref (ac_con));
}
for (i = 0; i < acs->len; i++)
{
NMConnection *con;
con = g_ptr_array_index (acs, i);
if (connection_ignored (con))
continue;
g_ptr_array_add (self->connections, g_object_ref (con));
if (self->hide_unavailable && con != ac_con)
g_ptr_array_add (self->connections_row, NULL);
else
g_ptr_array_add (self->connections_row,
cc_wifi_connection_list_row_add (self, con,
NULL));
}
/* Coldplug all known APs again */
aps = nm_device_wifi_get_access_points (self->device);
for (i = 0; i < aps->len; i++)
on_device_ap_added_cb (self, g_ptr_array_index (aps, i), self->device);
self->updating = FALSE;
}
static void
on_row_configured_cb (CcWifiConnectionList *self, CcWifiConnectionRow *row)
{
g_signal_emit_by_name (self, "configure", row);
}
static void
on_access_point_property_changed (CcWifiConnectionList *self,
GParamSpec *pspec,
NMAccessPoint *ap)
{
CcWifiConnectionRow *row;
GBytes *ssid;
gboolean has_connection = FALSE;
gint i;
/* If the SSID changed then the AP needs to be added/removed from rows.
* Do this by simulating an AP addition/removal. */
if (g_str_equal (pspec->name, NM_ACCESS_POINT_SSID))
{
g_debug ("Simulating add/remove for SSID change");
on_device_ap_removed_cb (self, ap, self->device);
on_device_ap_added_cb (self, ap, self->device);
return;
}
/* Otherwise, find all rows that contain the AP and update it. Do this by
* first searching all rows with connections, and then looking it up in the
* SSID rows if not found. */
for (i = 0; i < self->connections_row->len; i++)
{
row = g_ptr_array_index (self->connections_row, i);
if (row && cc_wifi_connection_row_has_access_point (row, ap))
{
cc_wifi_connection_row_update (row);
has_connection = TRUE;
}
}
if (!self->show_aps || has_connection)
return;
ssid = g_hash_table_lookup (self->ap_ssid_cache, ap);
if (!ssid)
return;
row = g_hash_table_lookup (self->ssid_to_row, ssid);
if (!row)
g_assert_not_reached ();
else
cc_wifi_connection_row_update (row);
}
static void
on_device_ap_added_cb (CcWifiConnectionList *self,
NMAccessPoint *ap,
NMDeviceWifi *device)
{
g_autoptr(GPtrArray) connections = NULL;
CcWifiConnectionRow *row;
GBytes *ap_ssid;
g_autoptr(GBytes) ssid = NULL;
guint i, j;
g_signal_connect_object (ap, "notify",
G_CALLBACK (on_access_point_property_changed),
self, G_CONNECT_SWAPPED);
connections = nm_access_point_filter_connections (ap, self->connections);
/* If this is the active AP, then add the active connection to the list. This
* is a workaround because nm_access_pointer_filter_connections() will not
* include it otherwise.
* So it seems like the dummy AP entry that NM creates internally is not actually
* compatible with the connection that is being activated.
*/
if (ap == nm_device_wifi_get_active_access_point (device))
{
NMActiveConnection *ac;
NMConnection *ac_con;
ac = nm_device_get_active_connection (NM_DEVICE (self->device));
if (ac)
{
guint idx;
ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac));
if (!g_ptr_array_find (connections, ac_con, NULL) &&
g_ptr_array_find (self->connections, ac_con, &idx))
{
g_debug ("Adding active connection to list of valid connections for AP");
g_ptr_array_add (connections, g_object_ref (ac_con));
}
}
}
/* Add the AP to all connection related rows, creating the row if neccessary. */
for (i = 0; i < connections->len; i++)
{
gboolean found = g_ptr_array_find (self->connections, g_ptr_array_index (connections, i), &j);
g_assert (found);
row = g_ptr_array_index (self->connections_row, j);
if (!row)
row = cc_wifi_connection_list_row_add (self, g_ptr_array_index (connections, i), NULL);
cc_wifi_connection_row_add_access_point (row, ap);
g_ptr_array_index (self->connections_row, j) = row;
}
if (connections->len > 0)
return;
if (!self->show_aps)
return;
/* The AP is not compatible to any known connection, generate an entry for the
* SSID or add to existing one. However, not for hidden APs that don't have an SSID.
*/
ap_ssid = nm_access_point_get_ssid (ap);
if (ap_ssid == NULL)
return;
ssid = new_hashable_ssid (ap_ssid);
g_hash_table_insert (self->ap_ssid_cache, ap, g_bytes_ref (ssid));
row = g_hash_table_lookup (self->ssid_to_row, ssid);
if (!row)
{
row = cc_wifi_connection_list_row_add (self, NULL, ap);
g_hash_table_insert (self->ssid_to_row, g_bytes_ref (ssid), row);
}
else
{
cc_wifi_connection_row_add_access_point (row, ap);
}
}
static void
on_device_ap_removed_cb (CcWifiConnectionList *self,
NMAccessPoint *ap,
NMDeviceWifi *device)
{
CcWifiConnectionRow *row;
g_autoptr(GBytes) ssid = NULL;
gboolean found = FALSE;
gint i;
g_signal_handlers_disconnect_by_data (ap, self);
/* Find any connection related row with the AP and remove the AP from it. Remove the
* row if it was the last AP and we are hiding unavailable connections. */
for (i = 0; i < self->connections_row->len; i++)
{
row = g_ptr_array_index (self->connections_row, i);
if (row && cc_wifi_connection_row_remove_access_point (row, ap))
{
found = TRUE;
if (self->hide_unavailable)
{
g_ptr_array_index (self->connections_row, i) = NULL;
gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (row));
}
}
}
if (found || !self->show_aps)
return;
/* If the AP was inserted into a row without a connection, then we will get an
* SSID for it here. */
g_hash_table_steal_extended (self->ap_ssid_cache, ap, NULL, (gpointer*) &ssid);
if (!ssid)
return;
/* And we can update the row (possibly removing it) */
row = g_hash_table_lookup (self->ssid_to_row, ssid);
g_assert (row != NULL);
if (cc_wifi_connection_row_remove_access_point (row, ap))
{
g_hash_table_remove (self->ssid_to_row, ssid);
gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (row));
}
}
static void
on_client_connection_added_cb (CcWifiConnectionList *self,
NMConnection *connection,
NMClient *client)
{
if (!nm_device_connection_compatible (NM_DEVICE (self->device), connection, NULL))
return;
if (connection_ignored (connection))
return;
/* The approach we take to handle connection changes is to do a full rebuild.
* It happens seldom enough to make this feasible.
*/
update_connections (self);
}
static void
on_client_connection_removed_cb (CcWifiConnectionList *self,
NMConnection *connection,
NMClient *client)
{
if (!g_ptr_array_find (self->connections, connection, NULL))
return;
/* The approach we take to handle connection changes is to do a full rebuild.
* It happens seldom enough to make this feasible.
*/
update_connections (self);
}
static void
on_device_state_changed_cb (CcWifiConnectionList *self,
GParamSpec *pspec,
NMDeviceWifi *device)
{
NMActiveConnection *ac;
NMConnection *connection = NULL;
guint idx;
ac = nm_device_get_active_connection (NM_DEVICE (self->device));
if (ac)
connection = NM_CONNECTION (nm_active_connection_get_connection (ac));
/* Just update the corresponding row if the AC is still the same. */
if (self->last_active == connection &&
g_ptr_array_find (self->connections, connection, &idx) &&
g_ptr_array_index (self->connections_row, idx))
{
cc_wifi_connection_row_update (g_ptr_array_index (self->connections_row, idx));
return;
}
/* Give up and do a full update. */
update_connections (self);
self->last_active = connection;
}
static void
on_device_active_ap_changed_cb (CcWifiConnectionList *self,
GParamSpec *pspec,
NMDeviceWifi *device)
{
NMAccessPoint *ap;
/* We need to make sure the active AP is grouped with the active connection.
* Do so by simply removing and adding it.
*
* This is necessary because the AP is added before this property
* is updated. */
ap = nm_device_wifi_get_active_access_point (self->device);
if (ap)
{
g_debug ("Simulating add/remove for active AP change");
on_device_ap_removed_cb (self, ap, self->device);
on_device_ap_added_cb (self, ap, self->device);
}
}
static void
cc_wifi_connection_list_dispose (GObject *object)
{
CcWifiConnectionList *self = (CcWifiConnectionList *)object;
/* Prevent any further updates; clear_widget must not indirectly recurse
* through updates_connections */
self->updating = TRUE;
/* Drop all external references */
clear_widget (self);
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->dispose (object);
}
static void
cc_wifi_connection_list_finalize (GObject *object)
{
CcWifiConnectionList *self = (CcWifiConnectionList *)object;
g_clear_object (&self->client);
g_clear_object (&self->device);
g_clear_pointer (&self->connections, g_ptr_array_unref);
g_clear_pointer (&self->connections_row, g_ptr_array_unref);
g_clear_pointer (&self->ssid_to_row, g_hash_table_unref);
g_clear_pointer (&self->ap_ssid_cache, g_hash_table_unref);
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->finalize (object);
}
static void
cc_wifi_connection_list_constructed (GObject *object)
{
CcWifiConnectionList *self = (CcWifiConnectionList *)object;
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->constructed (object);
g_assert (self->client);
g_assert (self->device);
g_signal_connect_object (self->client, "connection-added",
G_CALLBACK (on_client_connection_added_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->client, "connection-removed",
G_CALLBACK (on_client_connection_removed_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->device, "access-point-added",
G_CALLBACK (on_device_ap_added_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->device, "access-point-removed",
G_CALLBACK (on_device_ap_removed_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->device, "notify::state",
G_CALLBACK (on_device_state_changed_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->device, "notify::active-connection",
G_CALLBACK (on_device_state_changed_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->device, "notify::active-access-point",
G_CALLBACK (on_device_active_ap_changed_cb),
self, G_CONNECT_SWAPPED);
on_device_state_changed_cb (self, NULL, self->device);
/* Simulate a change notification on the available connections.
* This uses the implementation detail that the list is rebuild
* completely in this case. */
update_connections (self);
}
static void
cc_wifi_connection_list_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object);
switch (prop_id)
{
case PROP_CHECKABLE:
g_value_set_boolean (value, self->checkable);
break;
case PROP_HIDE_UNAVAILABLE:
g_value_set_boolean (value, self->hide_unavailable);
break;
case PROP_SHOW_APS:
g_value_set_boolean (value, self->show_aps);
break;
case PROP_CLIENT:
g_value_set_object (value, self->client);
break;
case PROP_DEVICE:
g_value_set_object (value, self->device);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
cc_wifi_connection_list_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object);
switch (prop_id)
{
case PROP_CHECKABLE:
self->checkable = g_value_get_boolean (value);
break;
case PROP_HIDE_UNAVAILABLE:
self->hide_unavailable = g_value_get_boolean (value);
break;
case PROP_SHOW_APS:
self->show_aps = g_value_get_boolean (value);
break;
case PROP_CLIENT:
self->client = g_value_dup_object (value);
break;
case PROP_DEVICE:
self->device = g_value_dup_object (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
cc_wifi_connection_list_class_init (CcWifiConnectionListClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = cc_wifi_connection_list_constructed;
object_class->dispose = cc_wifi_connection_list_dispose;
object_class->finalize = cc_wifi_connection_list_finalize;
object_class->get_property = cc_wifi_connection_list_get_property;
object_class->set_property = cc_wifi_connection_list_set_property;
props[PROP_CHECKABLE] =
g_param_spec_boolean ("checkable", "checkable",
"Passed to the created rows to show/hide the checkbox for deletion",
FALSE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
props[PROP_HIDE_UNAVAILABLE] =
g_param_spec_boolean ("hide-unavailable", "HideUnavailable",
"Whether to show or hide unavailable connections",
TRUE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
props[PROP_SHOW_APS] =
g_param_spec_boolean ("show-aps", "ShowAPs",
"Whether to show available SSIDs/APs without a connection",
TRUE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
props[PROP_CLIENT] =
g_param_spec_object ("client", "NMClient",
"The NM Client",
NM_TYPE_CLIENT,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
props[PROP_DEVICE] =
g_param_spec_object ("device", "WiFi Device",
"The WiFi Device for this connection list",
NM_TYPE_DEVICE_WIFI,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class,
PROP_LAST,
props);
g_signal_new ("configure",
CC_TYPE_WIFI_CONNECTION_LIST,
G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL,
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW);
}
static void
cc_wifi_connection_list_init (CcWifiConnectionList *self)
{
self->hide_unavailable = TRUE;
self->show_aps = TRUE;
self->connections = g_ptr_array_new_with_free_func (g_object_unref);
self->connections_row = g_ptr_array_new ();
self->ssid_to_row = g_hash_table_new_full (g_bytes_hash, g_bytes_equal,
(GDestroyNotify) g_bytes_unref, NULL);
self->ap_ssid_cache = g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, (GDestroyNotify) g_bytes_unref);
}
CcWifiConnectionList *
cc_wifi_connection_list_new (NMClient *client,
NMDeviceWifi *device,
gboolean hide_unavailable,
gboolean show_aps,
gboolean checkable)
{
return g_object_new (CC_TYPE_WIFI_CONNECTION_LIST,
"client", client,
"device", device,
"hide-unavailable", hide_unavailable,
"show-aps", show_aps,
"checkable", checkable,
NULL);
}
void
cc_wifi_connection_list_freeze (CcWifiConnectionList *self)
{
g_return_if_fail (CC_WIFI_CONNECTION_LIST (self));
if (self->freeze_count == 0)
g_debug ("wifi connection list has been frozen");
self->freeze_count += 1;
}
void
cc_wifi_connection_list_thaw (CcWifiConnectionList *self)
{
g_return_if_fail (CC_WIFI_CONNECTION_LIST (self));
g_return_if_fail (self->freeze_count > 0);
self->freeze_count -= 1;
if (self->freeze_count == 0)
{
g_debug ("wifi connection list has been thawed");
update_connections (self);
}
}