diff options
Diffstat (limited to '')
-rw-r--r-- | panels/network/cc-wifi-connection-list.c | 759 |
1 files changed, 759 insertions, 0 deletions
diff --git a/panels/network/cc-wifi-connection-list.c b/panels/network/cc-wifi-connection-list.c new file mode 100644 index 0000000..ff91892 --- /dev/null +++ b/panels/network/cc-wifi-connection-list.c @@ -0,0 +1,759 @@ +/* + * 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); + } +} |