Blob Blame History Raw
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2018 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-device-wifi-p2p.h"

#include <sys/socket.h>

#include "supplicant/nm-supplicant-manager.h"
#include "supplicant/nm-supplicant-interface.h"

#include "NetworkManagerUtils.h"
#include "devices/nm-device-private.h"
#include "nm-act-request.h"
#include "nm-core-internal.h"
#include "nm-glib-aux/nm-ref-string.h"
#include "nm-ip4-config.h"
#include "nm-manager.h"
#include "nm-manager.h"
#include "nm-setting-wifi-p2p.h"
#include "nm-utils.h"
#include "nm-wifi-p2p-peer.h"
#include "platform/nm-platform.h"
#include "platform/nmp-object.h"
#include "settings/nm-settings.h"

#define _NMLOG_DEVICE_TYPE NMDeviceWifiP2P
#include "devices/nm-device-logging.h"

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWifiP2P, PROP_PEERS, );

typedef struct {
    NMSupplicantManager *sup_mgr;

    /* NOTE: In theory management and group ifaces could be identical. However,
     * in practice, this cannot happen currently as NMDeviceWifiP2P is only
     * created for existing non-P2P interfaces.
     * (i.e. a single standalone P2P interface is not supported at this point)
     */
    NMSupplicantInterface *mgmt_iface;
    NMSupplicantInterface *group_iface;

    CList peers_lst_head;

    guint find_peer_timeout_id;
    guint sup_timeout_id;
    guint peer_dump_id;
    guint peer_missing_id;

    bool is_waiting_for_supplicant : 1;
} NMDeviceWifiP2PPrivate;

struct _NMDeviceWifiP2P {
    NMDevice               parent;
    NMDeviceWifiP2PPrivate _priv;
};

struct _NMDeviceWifiP2PClass {
    NMDeviceClass parent;
};

G_DEFINE_TYPE(NMDeviceWifiP2P, nm_device_wifi_p2p, NM_TYPE_DEVICE)

#define NM_DEVICE_WIFI_P2P_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMDeviceWifiP2P, NM_IS_DEVICE_WIFI_P2P, NMDevice)

/*****************************************************************************/

static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p;
static const GDBusSignalInfo             nm_signal_info_wifi_p2p_peer_added;
static const GDBusSignalInfo             nm_signal_info_wifi_p2p_peer_removed;

static void supplicant_group_interface_release(NMDeviceWifiP2P *self);
static void supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting);

/*****************************************************************************/

static void
_peer_dump(NMDeviceWifiP2P *    self,
           NMLogLevel           log_level,
           const NMWifiP2PPeer *peer,
           const char *         prefix,
           gint32               now_s)
{
    char buf[1024];

    _NMLOG(log_level,
           LOGD_WIFI_SCAN,
           "wifi-peer: %-7s %s",
           prefix,
           nm_wifi_p2p_peer_to_string(peer, buf, sizeof(buf), now_s));
}

static gboolean
peer_list_dump(gpointer user_data)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(user_data);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    priv->peer_dump_id = 0;

    if (_LOGD_ENABLED(LOGD_WIFI_SCAN)) {
        NMWifiP2PPeer *peer;
        gint32         now_s = nm_utils_get_monotonic_timestamp_sec();

        _LOGD(LOGD_WIFI_SCAN, "P2P Peers: [now:%u]", now_s);
        c_list_for_each_entry (peer, &priv->peers_lst_head, peers_lst)
            _peer_dump(self, LOGL_DEBUG, peer, "dump", now_s);
    }
    return G_SOURCE_REMOVE;
}

static void
schedule_peer_list_dump(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (!priv->peer_dump_id && _LOGD_ENABLED(LOGD_WIFI_SCAN))
        priv->peer_dump_id = g_timeout_add_seconds(1, peer_list_dump, self);
}

/*****************************************************************************/

static void
_set_is_waiting_for_supplicant(NMDeviceWifiP2P *self, gboolean is_waiting)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (priv->is_waiting_for_supplicant == (!!is_waiting))
        return;

    priv->is_waiting_for_supplicant = is_waiting;

    if (is_waiting)
        nm_device_add_pending_action(NM_DEVICE(self),
                                     NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
                                     TRUE);
    else
        nm_device_remove_pending_action(NM_DEVICE(self),
                                        NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
                                        TRUE);
}

/*****************************************************************************/

static gboolean
check_connection_peer_joined(NMDeviceWifiP2P *device)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(device);
    NMConnection *          conn = nm_device_get_applied_connection(NM_DEVICE(device));
    NMWifiP2PPeer *         peer;
    const char *            group;
    const char *const *     groups;

    if (!conn || !priv->group_iface)
        return FALSE;

    /* Comparing the object path found on the group_iface with the peers
     * found on the mgmt_iface is legal. */
    group = nm_supplicant_interface_get_p2p_group_path(priv->group_iface);
    if (!group)
        return FALSE;

    /* NOTE: We currently only support connections to a specific peer */
    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, conn);
    if (!peer)
        return FALSE;

    groups = nm_wifi_p2p_peer_get_groups(peer);
    if (!groups || !g_strv_contains(groups, group))
        return FALSE;

    return TRUE;
}

static gboolean
disconnect_on_connection_peer_missing_cb(gpointer user_data)
{
    NMDevice *              device = NM_DEVICE(user_data);
    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    _LOGW(LOGD_WIFI, "Peer requested in connection is missing for too long, failing connection.");

    priv->peer_missing_id = 0;

    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
    return FALSE;
}

static void
update_disconnect_on_connection_peer_missing(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMDeviceState           state;

    state = nm_device_get_state(NM_DEVICE(self));
    if (state < NM_DEVICE_STATE_IP_CONFIG || state > NM_DEVICE_STATE_ACTIVATED) {
        nm_clear_g_source(&priv->peer_missing_id);
        return;
    }

    if (check_connection_peer_joined(self)) {
        if (nm_clear_g_source(&priv->peer_missing_id))
            _LOGD(LOGD_WIFI, "Peer requested in connection is joined, removing timeout");
        return;
    }

    if (priv->peer_missing_id == 0) {
        _LOGD(LOGD_WIFI, "Peer requested in connection is missing, adding timeout");
        priv->peer_missing_id =
            g_timeout_add_seconds(5, disconnect_on_connection_peer_missing_cb, self);
    }
}

static gboolean
is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
{
    NMDeviceWifiP2P *          self = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *   priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMSupplicantInterfaceState supplicant_state;

    if (!priv->mgmt_iface)
        return FALSE;

    supplicant_state = nm_supplicant_interface_get_state(priv->mgmt_iface);
    return nm_supplicant_interface_state_is_operational(supplicant_state);
}

static gboolean
check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
{
    if (!NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class)
             ->check_connection_compatible(device, connection, error))
        return FALSE;

    /* TODO: Allow limitting the interface using the HW-address? */

    /* We don't need to check anything else here. The P2P device will only
     * exists if we are able to establish a P2P connection, and there should
     * be no further restrictions necessary.
     */

    return TRUE;
}

static gboolean
complete_connection(NMDevice *           device,
                    NMConnection *       connection,
                    const char *         specific_object,
                    NMConnection *const *existing_connections,
                    GError **            error)
{
    NMDeviceWifiP2P * self         = NM_DEVICE_WIFI_P2P(device);
    gs_free char *    setting_name = NULL;
    NMSettingWifiP2P *s_wifi_p2p;
    NMWifiP2PPeer *   peer;
    const char *      setting_peer;

    s_wifi_p2p =
        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));

    if (!specific_object) {
        /* If not given a specific object, we need at minimum a peer address */
        if (!s_wifi_p2p) {
            g_set_error(error,
                        NM_DEVICE_ERROR,
                        NM_DEVICE_ERROR_INVALID_CONNECTION,
                        "A '%s' setting is required if no Peer path was given",
                        NM_SETTING_WIFI_P2P_SETTING_NAME);
            return FALSE;
        }

        setting_peer = nm_setting_wifi_p2p_get_peer(s_wifi_p2p);
        if (!setting_peer) {
            g_set_error(error,
                        NM_DEVICE_ERROR,
                        NM_DEVICE_ERROR_INVALID_CONNECTION,
                        "A '%s' setting with a valid Peer is required if no Peer path was given",
                        NM_SETTING_WIFI_P2P_SETTING_NAME);
            return FALSE;
        }

    } else {
        peer = nm_wifi_p2p_peer_lookup_for_device(NM_DEVICE(self), specific_object);
        if (!peer) {
            g_set_error(error,
                        NM_DEVICE_ERROR,
                        NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND,
                        "The P2P peer %s is unknown",
                        specific_object);
            return FALSE;
        }

        setting_peer = nm_wifi_p2p_peer_get_address(peer);
        g_return_val_if_fail(setting_peer, FALSE);
    }

    /* Add a Wi-Fi P2P setting if one doesn't exist yet */
    if (!s_wifi_p2p) {
        s_wifi_p2p = NM_SETTING_WIFI_P2P(nm_setting_wifi_p2p_new());
        nm_connection_add_setting(connection, NM_SETTING(s_wifi_p2p));
    }

    g_object_set(G_OBJECT(s_wifi_p2p), NM_SETTING_WIFI_P2P_PEER, setting_peer, NULL);

    setting_name = g_strdup_printf("Wi-Fi P2P Peer %s", setting_peer);
    nm_utils_complete_generic(nm_device_get_platform(device),
                              connection,
                              NM_SETTING_WIFI_P2P_SETTING_NAME,
                              existing_connections,
                              setting_name,
                              setting_name,
                              NULL,
                              NULL,
                              TRUE);

    return TRUE;
}

/*
 * supplicant_find_timeout_cb
 *
 * Called when the supplicant has been unable to find the peer we want to connect to.
 */
static gboolean
supplicant_find_timeout_cb(gpointer user_data)
{
    NMDevice *              device = NM_DEVICE(user_data);
    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(user_data);
    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    priv->find_peer_timeout_id = 0;

    nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);

    if (nm_device_is_activating(device)) {
        _LOGW(LOGD_DEVICE | LOGD_WIFI,
              "Activation: (wifi-p2p) could not find peer, failing activation");
        nm_device_state_changed(device,
                                NM_DEVICE_STATE_FAILED,
                                NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
    }

    return G_SOURCE_REMOVE;
}

static NMActStageReturn
act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMConnection *          connection;
    NMSettingWifiP2P *      s_wifi_p2p;
    NMWifiP2PPeer *         peer;

    if (!priv->mgmt_iface) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    connection = nm_device_get_applied_connection(NM_DEVICE(self));
    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);

    s_wifi_p2p =
        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
    g_return_val_if_fail(s_wifi_p2p, NM_ACT_STAGE_RETURN_FAILURE);

    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
    if (!peer) {
        /* Set up a timeout on the find attempt and run a find for the same period of time */
        if (priv->find_peer_timeout_id == 0) {
            priv->find_peer_timeout_id =
                g_timeout_add_seconds(10, supplicant_find_timeout_cb, self);

            nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, 10);
        }
        return NM_ACT_STAGE_RETURN_POSTPONE;
    }

    return NM_ACT_STAGE_RETURN_SUCCESS;
}

/*
 * supplicant_connection_timeout_cb
 *
 * Called when the supplicant has been unable to connect to a peer
 * within a specified period of time.
 */
static gboolean
supplicant_connection_timeout_cb(gpointer user_data)
{
    NMDevice *              device = NM_DEVICE(user_data);
    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(user_data);
    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    priv->sup_timeout_id = 0;

    nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);

    if (nm_device_is_activating(device)) {
        _LOGW(LOGD_DEVICE | LOGD_WIFI,
              "Activation: (wifi-p2p) connecting took too long, failing activation");
        nm_device_state_changed(device,
                                NM_DEVICE_STATE_FAILED,
                                NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
    }

    return G_SOURCE_REMOVE;
}

static NMActStageReturn
act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMConnection *          connection;
    NMSettingWifiP2P *      s_wifi_p2p;
    NMWifiP2PPeer *         peer;
    GBytes *                wfd_ies;

    if (nm_clear_g_source(&priv->find_peer_timeout_id))
        nm_assert_not_reached();

    if (!priv->mgmt_iface) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    connection = nm_device_get_applied_connection(device);
    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
    nm_assert(
        NM_IS_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)));

    /* The prepare stage ensures that the peer has been found */
    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
    if (!peer) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    /* Set the WFD IEs before trying to establish the connection. */
    s_wifi_p2p =
        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
    wfd_ies = nm_setting_wifi_p2p_get_wfd_ies(s_wifi_p2p);
    nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, wfd_ies);

    /* TODO: Grab secrets if we don't have them yet! */

    /* TODO: Fix "pbc" being hardcoded here! */
    nm_supplicant_interface_p2p_connect(priv->mgmt_iface,
                                        nm_wifi_p2p_peer_get_supplicant_path(peer),
                                        "pbc",
                                        NULL);

    /* Set up a timeout on the connect attempt */
    if (priv->sup_timeout_id == 0) {
        priv->sup_timeout_id = g_timeout_add_seconds(45, supplicant_connection_timeout_cb, self);
    }

    /* We'll get stage3 started when the P2P group has been started */
    return NM_ACT_STAGE_RETURN_POSTPONE;
}

/*****************************************************************************/

static void
emit_signal_p2p_peer_add_remove(NMDeviceWifiP2P *device,
                                NMWifiP2PPeer *  peer,
                                gboolean         is_added /* or else is_removed */)
{
    nm_dbus_object_emit_signal(NM_DBUS_OBJECT(device),
                               &interface_info_device_wifi_p2p,
                               is_added ? &nm_signal_info_wifi_p2p_peer_added
                                        : &nm_signal_info_wifi_p2p_peer_removed,
                               "(o)",
                               nm_dbus_object_get_path(NM_DBUS_OBJECT(peer)));
}

static void
peer_add_remove(NMDeviceWifiP2P *self,
                gboolean         is_adding, /* or else removing */
                NMWifiP2PPeer *  peer,
                gboolean         recheck_available_connections)
{
    NMDevice *              device = NM_DEVICE(self);
    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (is_adding) {
        g_object_ref(peer);
        peer->wifi_device = device;
        c_list_link_tail(&priv->peers_lst_head, &peer->peers_lst);
        nm_dbus_object_export(NM_DBUS_OBJECT(peer));
        _peer_dump(self, LOGL_DEBUG, peer, "added", 0);

        emit_signal_p2p_peer_add_remove(self, peer, TRUE);
    } else {
        peer->wifi_device = NULL;
        c_list_unlink(&peer->peers_lst);
        _peer_dump(self, LOGL_DEBUG, peer, "removed", 0);
    }

    _notify(self, PROP_PEERS);

    if (!is_adding) {
        emit_signal_p2p_peer_add_remove(self, peer, FALSE);
        nm_dbus_object_clear_and_unexport(&peer);
    }

    if (is_adding) {
        /* If we are in prepare state, then we are currently runnign a find
         * to search for the requested peer. */
        if (priv->find_peer_timeout_id != 0) {
            NMConnection *connection;

            nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_PREPARE);

            connection = nm_device_get_applied_connection(device);
            nm_assert(NM_IS_CONNECTION(connection));

            peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
            if (peer) {
                /* A peer for the connection was found, cancel the timeout and go to configure state. */
                nm_clear_g_source(&priv->find_peer_timeout_id);
                nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
            }
        }

        /* TODO: We may want to re-check auto-activation here, otherwise it will never work. */
    }

    update_disconnect_on_connection_peer_missing(self);
}

static void
remove_all_peers(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMWifiP2PPeer *         peer;

    if (c_list_is_empty(&priv->peers_lst_head))
        return;

    while ((peer = c_list_first_entry(&priv->peers_lst_head, NMWifiP2PPeer, peers_lst)))
        peer_add_remove(self, FALSE, peer, FALSE);

    nm_device_recheck_available_connections(NM_DEVICE(self));
}

/*****************************************************************************/

static NMActStageReturn
act_stage3_ip_config_start(NMDevice *           device,
                           int                  addr_family,
                           gpointer *           out_config,
                           NMDeviceStateReason *out_failure_reason)
{
    gboolean      indicate_addressing_running;
    NMConnection *connection;
    const char *  method;

    connection = nm_device_get_applied_connection(device);

    method = nm_utils_get_ip_config_method(connection, addr_family);

    if (addr_family == AF_INET)
        indicate_addressing_running = NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
    else {
        indicate_addressing_running = NM_IN_STRSET(method,
                                                   NM_SETTING_IP6_CONFIG_METHOD_AUTO,
                                                   NM_SETTING_IP6_CONFIG_METHOD_DHCP);
    }

    if (indicate_addressing_running)
        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
                                                     nm_device_get_ip_ifindex(device),
                                                     TRUE);

    return NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class)
        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
}

static void
deactivate(NMDevice *device)
{
    NMDeviceWifiP2P *       self    = NM_DEVICE_WIFI_P2P(device);
    int                     ifindex = nm_device_get_ip_ifindex(device);
    NMDeviceWifiP2PPrivate *priv    = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    nm_clear_g_source(&priv->find_peer_timeout_id);
    nm_clear_g_source(&priv->sup_timeout_id);
    nm_clear_g_source(&priv->peer_missing_id);

    if (priv->mgmt_iface)
        nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);

    if (priv->group_iface)
        nm_supplicant_interface_p2p_disconnect(priv->group_iface);

    /* Clear any critical protocol notification in the Wi-Fi stack */
    if (ifindex > 0)
        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
                                                     ifindex,
                                                     FALSE);
}

static guint32
get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
{
    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
    return 0;
}

static const char *
get_auto_ip_config_method(NMDevice *device, int addr_family)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    /* Override the AUTO method to mean shared if we are group owner. */
    if (priv->group_iface && nm_supplicant_interface_get_p2p_group_owner(priv->group_iface)) {
        if (addr_family == AF_INET)
            return NM_SETTING_IP4_CONFIG_METHOD_SHARED;

        if (addr_family == AF_INET6)
            return NM_SETTING_IP6_CONFIG_METHOD_SHARED;
    }

    return NULL;
}

static gboolean
unmanaged_on_quit(NMDevice *self)
{
    return TRUE;
}

static void
supplicant_iface_state_cb(NMSupplicantInterface *iface,
                          int                    new_state_i,
                          int                    old_state_i,
                          int                    disconnect_reason,
                          gpointer               user_data)
{
    NMDeviceWifiP2P *          self      = NM_DEVICE_WIFI_P2P(user_data);
    NMDevice *                 device    = NM_DEVICE(self);
    NMSupplicantInterfaceState new_state = new_state_i;
    NMSupplicantInterfaceState old_state = old_state_i;

    _LOGI(LOGD_DEVICE | LOGD_WIFI,
          "supplicant management interface state: %s -> %s",
          nm_supplicant_interface_state_to_string(old_state),
          nm_supplicant_interface_state_to_string(new_state));

    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
        supplicant_interfaces_release(self, TRUE);
        nm_device_queue_recheck_available(device,
                                          NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
                                          NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
        return;
    }

    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
        _LOGD(LOGD_WIFI, "supplicant ready");
        nm_device_queue_recheck_available(device,
                                          NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
                                          NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
        _set_is_waiting_for_supplicant(self, FALSE);
    }
}

static void
supplicant_iface_peer_changed_cb(NMSupplicantInterface *iface,
                                 NMSupplicantPeerInfo * peer_info,
                                 gboolean               is_present,
                                 NMDeviceWifiP2P *      self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    NMWifiP2PPeer *         found_peer;

    found_peer =
        nm_wifi_p2p_peers_find_by_supplicant_path(&priv->peers_lst_head, peer_info->peer_path->str);

    if (!is_present) {
        if (!found_peer)
            return;

        peer_add_remove(self, FALSE, found_peer, TRUE);
        goto out;
    }

    if (found_peer) {
        if (!nm_wifi_p2p_peer_update_from_properties(found_peer, peer_info))
            return;

        update_disconnect_on_connection_peer_missing(self);
        _peer_dump(self, LOGL_DEBUG, found_peer, "updated", 0);
    } else {
        gs_unref_object NMWifiP2PPeer *peer = NULL;

        peer = nm_wifi_p2p_peer_new_from_properties(peer_info);
        peer_add_remove(self, TRUE, peer, TRUE);
    }

out:
    schedule_peer_list_dump(self);
}

static void
check_group_iface_ready(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (!priv->group_iface)
        return;

    if (!nm_supplicant_interface_state_is_operational(
            nm_supplicant_interface_get_state(priv->group_iface)))
        return;

    if (!nm_supplicant_interface_get_p2p_group_joined(priv->group_iface))
        return;

    nm_clear_g_source(&priv->sup_timeout_id);
    update_disconnect_on_connection_peer_missing(self);

    nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self));
}

static void
supplicant_group_iface_is_ready(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    _LOGD(LOGD_WIFI, "P2P Group supplicant ready");

    if (!nm_device_set_ip_iface(NM_DEVICE(self),
                                nm_supplicant_interface_get_ifname(priv->group_iface))) {
        nm_device_state_changed(NM_DEVICE(self),
                                NM_DEVICE_STATE_FAILED,
                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
        return;
    }

    _set_is_waiting_for_supplicant(self, FALSE);
    check_group_iface_ready(self);
}

static void
supplicant_group_iface_state_cb(NMSupplicantInterface *iface,
                                int                    new_state_i,
                                int                    old_state_i,
                                int                    disconnect_reason,
                                gpointer               user_data)
{
    NMDeviceWifiP2P *          self      = NM_DEVICE_WIFI_P2P(user_data);
    NMSupplicantInterfaceState new_state = new_state_i;
    NMSupplicantInterfaceState old_state = old_state_i;

    _LOGI(LOGD_DEVICE | LOGD_WIFI,
          "P2P Group supplicant interface state: %s -> %s",
          nm_supplicant_interface_state_to_string(old_state),
          nm_supplicant_interface_state_to_string(new_state));

    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
        supplicant_group_interface_release(self);

        nm_device_state_changed(NM_DEVICE(self),
                                NM_DEVICE_STATE_DISCONNECTED,
                                NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
        return;
    }

    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
        supplicant_group_iface_is_ready(self);
        return;
    }
}

static void
supplicant_group_iface_group_finished_cb(NMSupplicantInterface *iface,
                                         const char *           iface_path,
                                         void *                 user_data)
{
    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data);

    supplicant_group_interface_release(self);

    nm_device_state_changed(NM_DEVICE(self),
                            NM_DEVICE_STATE_DISCONNECTED,
                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
}

static void
supplicant_iface_group_joined_updated_cb(NMSupplicantInterface *iface,
                                         GParamSpec *           pspec,
                                         void *                 user_data)
{
    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data);

    check_group_iface_ready(self);
}

static void
supplicant_iface_group_started_cb(NMSupplicantInterface *iface,
                                  NMSupplicantInterface *group_iface,
                                  NMDeviceWifiP2P *      self)
{
    NMDeviceWifiP2PPrivate *   priv;
    NMSupplicantInterfaceState state;

    g_return_if_fail(self);

    if (!nm_device_is_activating(NM_DEVICE(self))) {
        _LOGW(LOGD_DEVICE | LOGD_WIFI,
              "P2P: WPA supplicant notified a group start but we are not trying to connect! "
              "Ignoring the event.");
        return;
    }

    priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    supplicant_group_interface_release(self);

    priv->group_iface = g_object_ref(group_iface);

    /* We need to wait for the interface to be ready and the group
     * information to be resolved. */
    g_signal_connect(priv->group_iface,
                     "notify::" NM_SUPPLICANT_INTERFACE_P2P_GROUP_JOINED,
                     G_CALLBACK(supplicant_iface_group_joined_updated_cb),
                     self);

    g_signal_connect(priv->group_iface,
                     NM_SUPPLICANT_INTERFACE_STATE,
                     G_CALLBACK(supplicant_group_iface_state_cb),
                     self);

    g_signal_connect(priv->group_iface,
                     NM_SUPPLICANT_INTERFACE_GROUP_FINISHED,
                     G_CALLBACK(supplicant_group_iface_group_finished_cb),
                     self);

    state = nm_supplicant_interface_get_state(priv->group_iface);
    if (state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
        _set_is_waiting_for_supplicant(self, TRUE);
        return;
    }

    supplicant_group_iface_is_ready(self);
}

static void
supplicant_group_interface_release(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (!priv->group_iface)
        return;

    g_signal_handlers_disconnect_by_data(priv->group_iface, self);

    nm_supplicant_interface_p2p_disconnect(priv->group_iface);

    g_clear_object(&priv->group_iface);
}

static void
supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    nm_clear_g_source(&priv->peer_dump_id);

    remove_all_peers(self);

    if (priv->mgmt_iface) {
        _LOGD(LOGD_DEVICE | LOGD_WIFI, "P2P: Releasing WPA supplicant interface.");
        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
        g_signal_handlers_disconnect_by_data(priv->mgmt_iface, self);
        g_clear_object(&priv->mgmt_iface);
        nm_clear_g_source(&priv->find_peer_timeout_id);
        nm_clear_g_source(&priv->sup_timeout_id);
    }

    supplicant_group_interface_release(self);

    if (set_is_waiting)
        _set_is_waiting_for_supplicant(self, TRUE);
}

static void
device_state_changed(NMDevice *          device,
                     NMDeviceState       new_state,
                     NMDeviceState       old_state,
                     NMDeviceStateReason reason)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    update_disconnect_on_connection_peer_missing(self);

    if (new_state <= NM_DEVICE_STATE_UNAVAILABLE) {
        /* Clean up the supplicant interface because in these states the
         * device cannot be used.
         * Do not clean up for the UNMANAGED to UNAVAILABLE transition which
         * will happen during initialization.
         */
        if (priv->mgmt_iface && old_state > new_state)
            supplicant_interfaces_release(self, TRUE);

        /* TODO: More cleanup needed? */
    }

    switch (new_state) {
    case NM_DEVICE_STATE_UNMANAGED:
        break;
    case NM_DEVICE_STATE_UNAVAILABLE:
        if (!priv->mgmt_iface
            || !nm_supplicant_interface_state_is_operational(
                nm_supplicant_interface_get_state(priv->mgmt_iface)))
            _set_is_waiting_for_supplicant(self, TRUE);
        break;
    case NM_DEVICE_STATE_NEED_AUTH:
        /* Disconnect? */
        break;
    case NM_DEVICE_STATE_IP_CHECK:
        /* Clear any critical protocol notification in the wifi stack */
        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
                                                     nm_device_get_ip_ifindex(device),
                                                     FALSE);
        break;
    case NM_DEVICE_STATE_ACTIVATED:
        //activation_success_handler (device);
        break;
    case NM_DEVICE_STATE_FAILED:
        /* Clear any critical protocol notification in the wifi stack.
         * At this point the IP device may have been removed already. */
        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
        if (nm_device_get_ip_ifindex(device) > 0)
            nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
                                                         nm_device_get_ip_ifindex(device),
                                                         FALSE);
        break;
    case NM_DEVICE_STATE_DISCONNECTED:
        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
        break;
    default:
        break;
    }
}

static void
impl_device_wifi_p2p_start_find(NMDBusObject *                     obj,
                                const NMDBusInterfaceInfoExtended *interface_info,
                                const NMDBusMethodInfoExtended *   method_info,
                                GDBusConnection *                  connection,
                                const char *                       sender,
                                GDBusMethodInvocation *            invocation,
                                GVariant *                         parameters)
{
    NMDeviceWifiP2P *       self       = NM_DEVICE_WIFI_P2P(obj);
    NMDeviceWifiP2PPrivate *priv       = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    gs_unref_variant GVariant *options = NULL;
    const char *               opts_key;
    GVariant *                 opts_val;
    GVariantIter               iter;
    gint32                     timeout = 30;

    g_variant_get(parameters, "(@a{sv})", &options);

    g_variant_iter_init(&iter, options);
    while (g_variant_iter_next(&iter, "{&sv}", &opts_key, &opts_val)) {
        _nm_unused gs_unref_variant GVariant *opts_val_free = opts_val;

        if (nm_streq(opts_key, "timeout")) {
            if (!g_variant_is_of_type(opts_val, G_VARIANT_TYPE_INT32)) {
                g_dbus_method_invocation_return_error_literal(
                    invocation,
                    NM_DEVICE_ERROR,
                    NM_DEVICE_ERROR_INVALID_ARGUMENT,
                    "\"timeout\" must be an integer \"i\"");
                return;
            }

            timeout = g_variant_get_int32(opts_val);
            if (timeout <= 0 || timeout > 600) {
                g_dbus_method_invocation_return_error_literal(
                    invocation,
                    NM_DEVICE_ERROR,
                    NM_DEVICE_ERROR_NOT_ALLOWED,
                    "The timeout for a find operation needs to be in the range of 1-600s.");
                return;
            }

            continue;
        }

        g_dbus_method_invocation_return_error(invocation,
                                              NM_DEVICE_ERROR,
                                              NM_DEVICE_ERROR_INVALID_ARGUMENT,
                                              "Unsupported options key \"%s\"",
                                              opts_key);
        return;
    }

    if (!priv->mgmt_iface) {
        g_dbus_method_invocation_return_error_literal(
            invocation,
            NM_DEVICE_ERROR,
            NM_DEVICE_ERROR_NOT_ACTIVE,
            "WPA Supplicant management interface is currently unavailable.");
        return;
    }

    nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, timeout);

    g_dbus_method_invocation_return_value(invocation, NULL);
}

static void
impl_device_wifi_p2p_stop_find(NMDBusObject *                     obj,
                               const NMDBusInterfaceInfoExtended *interface_info,
                               const NMDBusMethodInfoExtended *   method_info,
                               GDBusConnection *                  connection,
                               const char *                       sender,
                               GDBusMethodInvocation *            invocation,
                               GVariant *                         parameters)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(obj);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (!priv->mgmt_iface) {
        g_dbus_method_invocation_return_error_literal(
            invocation,
            NM_DEVICE_ERROR,
            NM_DEVICE_ERROR_NOT_ACTIVE,
            "WPA Supplicant management interface is currently unavailable.");
        return;
    }

    nm_supplicant_interface_p2p_stop_find(priv->mgmt_iface);

    g_dbus_method_invocation_return_value(invocation, NULL);
}

/*****************************************************************************/

NMSupplicantInterface *
nm_device_wifi_p2p_get_mgmt_iface(NMDeviceWifiP2P *self)
{
    g_return_val_if_fail(NM_IS_DEVICE_WIFI_P2P(self), NULL);

    return NM_DEVICE_WIFI_P2P_GET_PRIVATE(self)->mgmt_iface;
}

void
nm_device_wifi_p2p_set_mgmt_iface(NMDeviceWifiP2P *self, NMSupplicantInterface *iface)
{
    NMDeviceWifiP2PPrivate *priv;

    g_return_if_fail(NM_IS_DEVICE_WIFI_P2P(self));
    g_return_if_fail(!iface || NM_IS_SUPPLICANT_INTERFACE(iface));

    priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    if (priv->mgmt_iface == iface)
        goto done;

    supplicant_interfaces_release(self, FALSE);

    if (!iface)
        goto done;

    _LOGD(LOGD_DEVICE | LOGD_WIFI,
          "P2P: WPA supplicant management interface changed to %s.",
          nm_ref_string_get_str(nm_supplicant_interface_get_object_path(iface)));

    priv->mgmt_iface = g_object_ref(iface);

    g_signal_connect(priv->mgmt_iface,
                     NM_SUPPLICANT_INTERFACE_STATE,
                     G_CALLBACK(supplicant_iface_state_cb),
                     self);
    g_signal_connect(priv->mgmt_iface,
                     NM_SUPPLICANT_INTERFACE_PEER_CHANGED,
                     G_CALLBACK(supplicant_iface_peer_changed_cb),
                     self);
    g_signal_connect(priv->mgmt_iface,
                     NM_SUPPLICANT_INTERFACE_GROUP_STARTED,
                     G_CALLBACK(supplicant_iface_group_started_cb),
                     self);
done:
    nm_device_queue_recheck_available(NM_DEVICE(self),
                                      NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
                                      NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
    _set_is_waiting_for_supplicant(self,
                                   !priv->mgmt_iface
                                       || !nm_supplicant_interface_state_is_operational(
                                           nm_supplicant_interface_get_state(priv->mgmt_iface)));
}

void
nm_device_wifi_p2p_remove(NMDeviceWifiP2P *self)
{
    g_signal_emit_by_name(self, NM_DEVICE_REMOVED);
}

/*****************************************************************************/

static const char *
get_type_description(NMDevice *device)
{
    return "wifi-p2p";
}

/*****************************************************************************/

static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_added = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
    "PeerAdded",
    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), );

static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_removed =
    NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
        "PeerRemoved",
        .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), );

static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_DEVICE_WIFI_P2P,
        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
                    "StartFind",
                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                        NM_DEFINE_GDBUS_ARG_INFO("options", "a{sv}"), ), ),
                .handle = impl_device_wifi_p2p_start_find, ),
            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("StopFind", ),
                                                .handle = impl_device_wifi_p2p_stop_find, ), ),
        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_wifi_p2p_peer_added,
                                                &nm_signal_info_wifi_p2p_peer_removed, ),
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Peers",
                                                           "ao",
                                                           NM_DEVICE_WIFI_P2P_PEERS), ), ),
    .legacy_property_changed = FALSE,
};

/*****************************************************************************/

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(object);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
    const char **           list;

    switch (prop_id) {
    case PROP_PEERS:
        list = nm_wifi_p2p_peers_get_paths(&priv->peers_lst_head);
        g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

/*****************************************************************************/

static void
nm_device_wifi_p2p_init(NMDeviceWifiP2P *self)
{
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);

    c_list_init(&priv->peers_lst_head);

    priv->sup_mgr = g_object_ref(nm_supplicant_manager_get());
}

static void
constructed(GObject *object)
{
    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(object);

    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->constructed(object);

    _set_is_waiting_for_supplicant(self, TRUE);
}

NMDeviceWifiP2P *
nm_device_wifi_p2p_new(const char *iface)
{
    return g_object_new(NM_TYPE_DEVICE_WIFI_P2P,
                        NM_DEVICE_IFACE,
                        iface,
                        NM_DEVICE_TYPE_DESC,
                        "802.11 Wi-Fi P2P",
                        NM_DEVICE_DEVICE_TYPE,
                        NM_DEVICE_TYPE_WIFI_P2P,
                        NM_DEVICE_LINK_TYPE,
                        NM_LINK_TYPE_WIFI,
                        NM_DEVICE_RFKILL_TYPE,
                        RFKILL_TYPE_WLAN,
                        NULL);
}

static void
dispose(GObject *object)
{
    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(object);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(object);

    g_clear_object(&priv->sup_mgr);

    supplicant_interfaces_release(self, FALSE);

    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->dispose(object);
}

static void
finalize(GObject *object)
{
    NMDeviceWifiP2P *       peer = NM_DEVICE_WIFI_P2P(object);
    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(peer);

    nm_assert(c_list_is_empty(&priv->peers_lst_head));

    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->finalize(object);
}

static void
nm_device_wifi_p2p_class_init(NMDeviceWifiP2PClass *klass)
{
    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);

    object_class->constructed  = constructed;
    object_class->get_property = get_property;
    object_class->dispose      = dispose;
    object_class->finalize     = finalize;

    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wifi_p2p);

    device_class->connection_type_supported        = NM_SETTING_WIFI_P2P_SETTING_NAME;
    device_class->connection_type_check_compatible = NM_SETTING_WIFI_P2P_SETTING_NAME;
    device_class->link_types           = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI_P2P);
    device_class->get_type_description = get_type_description;

    /* Do we need compatibility checking or is the default good enough? */
    device_class->is_available                = is_available;
    device_class->check_connection_compatible = check_connection_compatible;
    device_class->complete_connection         = complete_connection;

    device_class->act_stage1_prepare         = act_stage1_prepare;
    device_class->act_stage2_config          = act_stage2_config;
    device_class->get_configured_mtu         = get_configured_mtu;
    device_class->get_auto_ip_config_method  = get_auto_ip_config_method;
    device_class->act_stage3_ip_config_start = act_stage3_ip_config_start;

    device_class->deactivate        = deactivate;
    device_class->unmanaged_on_quit = unmanaged_on_quit;

    device_class->state_changed = device_state_changed;

    obj_properties[PROP_PEERS] = g_param_spec_boxed(NM_DEVICE_WIFI_P2P_PEERS,
                                                    "",
                                                    "",
                                                    G_TYPE_STRV,
                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}