/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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:
 *
 * Copyright (C) 2011 - 2012 Google, Inc.
 * Copyright (c) 2022 Qualcomm Innovation Center, Inc.
 */

#include <stdlib.h>
#include <string.h>

#include <ModemManager.h>
#define _LIBMM_INSIDE_MM
#include <libmm-glib.h>

#include "mm-iface-modem.h"
#include "mm-iface-modem-location.h"
#include "mm-iface-modem-3gpp.h"
#include "mm-base-modem.h"
#include "mm-modem-helpers.h"
#include "mm-error-helpers.h"
#include "mm-log.h"

#define SUBSYSTEM_3GPP "3gpp"

/* When comparing EPS bearer settings take into account that:
 *  -  'password' may not always be readable.
 *  -  'apn-type' may not always be supported.
 *  -  'access-type-preference' may not always be reported.
 *  -  'profile-id' will not be known in the requested settings
 *  -  we ignore settings not applicable to profiles, like 'allow-roaming' or
 *     'rm-protocol'.
 *  -  we apply very loose matching for all fields.
 */
#define MM_BEARER_PROPERTIES_CMP_FLAGS_EPS              \
    (MM_BEARER_PROPERTIES_CMP_FLAGS_LOOSE |             \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_PROFILE_ID |     \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_PASSWORD |       \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_ALLOW_ROAMING |  \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_APN_TYPE |       \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_RM_PROTOCOL |    \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_ACCESS_TYPE_PREFERENCE | \
     MM_BEARER_PROPERTIES_CMP_FLAGS_NO_ROAMING_ALLOWANCE)


/*****************************************************************************/
/* Private data context */

#define PRIVATE_TAG "iface-modem-3gpp-private-tag"
static GQuark private_quark;

typedef struct {
    /* Registration state */
    MMModem3gppRegistrationState  state_cs;
    MMModem3gppRegistrationState  state_ps;
    MMModem3gppRegistrationState  state_eps;
    MMModem3gppRegistrationState  state_5gs;
    gboolean                      manual_registration;
    gchar                        *manual_registration_operator_id;
    GCancellable                 *pending_registration_cancellable;
    gboolean                      reloading_registration_info;
    /* Registration checks */
    guint    check_timeout_source;
    gboolean check_running;
} Private;

static void
private_free (Private *priv)
{
    g_free (priv->manual_registration_operator_id);
    if (priv->pending_registration_cancellable) {
        g_cancellable_cancel (priv->pending_registration_cancellable);
        g_object_unref (priv->pending_registration_cancellable);
    }
    if (priv->check_timeout_source)
        g_source_remove (priv->check_timeout_source);
    g_slice_free (Private, priv);
}

static Private *
get_private (MMIfaceModem3gpp *self)
{
    Private *priv;

    if (G_UNLIKELY (!private_quark))
        private_quark = g_quark_from_static_string (PRIVATE_TAG);

    priv = g_object_get_qdata (G_OBJECT (self), private_quark);
    if (!priv) {
        priv = g_slice_new0 (Private);
        priv->state_cs = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
        priv->state_ps = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
        priv->state_eps = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
        priv->state_5gs = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
        g_object_set_qdata_full (G_OBJECT (self), private_quark, priv, (GDestroyNotify)private_free);
    }

    return priv;
}

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

#define GET_NETWORK_SUPPORTED(domain,DOMAIN)                                         \
    static gboolean                                                                  \
    get_##domain##_network_supported (MMIfaceModem3gpp *self)                        \
    {                                                                                \
        gboolean supported = FALSE;                                                  \
                                                                                     \
        g_object_get (self,                                                          \
                      MM_IFACE_MODEM_3GPP_##DOMAIN##_NETWORK_SUPPORTED, &supported,  \
                      NULL);                                                         \
        return supported;                                                            \
    }

GET_NETWORK_SUPPORTED (cs,  CS)
GET_NETWORK_SUPPORTED (ps,  PS)
GET_NETWORK_SUPPORTED (eps, EPS)
GET_NETWORK_SUPPORTED (5gs, 5GS)

/*****************************************************************************/
/* Helper method to wait for a final packet service state */

typedef struct {
    MMModem3gppPacketServiceState final_state;
    gulong packet_service_state_changed_id;
    guint packet_service_state_changed_wait_id;
} WaitForPacketServiceStateContext;

MMModem3gppPacketServiceState
mm_iface_modem_3gpp_wait_for_packet_service_state_finish (MMIfaceModem3gpp  *self,
                                                          GAsyncResult      *res,
                                                          GError           **error)
{
    GError *inner_error = NULL;
    gssize  value;

    value = g_task_propagate_int (G_TASK (res), &inner_error);
    if (inner_error) {
        g_propagate_error (error, inner_error);
        return MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN;
    }
    return (MMModem3gppPacketServiceState)value;
}

static void
wait_for_packet_service_state_context_complete (GTask                         *task,
                                                MMModem3gppPacketServiceState  state,
                                                GError                        *error)
{
    MMIfaceModem3gpp                 *self;
    WaitForPacketServiceStateContext *ctx;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    if (ctx->packet_service_state_changed_id) {
        if (g_signal_handler_is_connected (self, ctx->packet_service_state_changed_id))
            g_signal_handler_disconnect (self, ctx->packet_service_state_changed_id);
        ctx->packet_service_state_changed_id = 0;
    }

    if (ctx->packet_service_state_changed_wait_id) {
        g_source_remove (ctx->packet_service_state_changed_wait_id);
        ctx->packet_service_state_changed_wait_id = 0;
    }

    if (error)
        g_task_return_error (task, error);
    else
        g_task_return_int (task, state);

    g_object_unref (task);
}

static gboolean
packet_service_state_changed_wait_expired (GTask *task)
{
    GError *error;

    error = g_error_new (MM_CORE_ERROR,
                         MM_CORE_ERROR_RETRY,
                         "Too much time waiting to get to a final packet service state");
    wait_for_packet_service_state_context_complete (task, MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN, error);
    return G_SOURCE_REMOVE;
}

static void
packet_service_state_changed (MMIfaceModem3gpp *self,
                              GParamSpec       *spec,
                              GTask            *task)
{
    WaitForPacketServiceStateContext *ctx;
    MMModem3gppPacketServiceState     state = MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE, &state,
                  NULL);

    ctx = g_task_get_task_data (task);

    /* If we want a specific final state and this is not the one we were
     * looking for, then skip */
    if ((ctx->final_state != MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN) &&
        (state != MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN) &&
        (state != ctx->final_state))
        return;

    /* Done! */
    wait_for_packet_service_state_context_complete (task, state, NULL);
}

void
mm_iface_modem_3gpp_wait_for_packet_service_state (MMIfaceModem3gpp              *self,
                                                   MMModem3gppPacketServiceState  final_state,
                                                   GAsyncReadyCallback            callback,
                                                   gpointer                       user_data)
{
    MMModem3gppPacketServiceState     state = MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN;
    WaitForPacketServiceStateContext *ctx;
    GTask                            *task;

    task = g_task_new (self, NULL, callback, user_data);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE, &state,
                  NULL);

    /* Is this the state we actually wanted? */
    if (final_state == MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN ||
        (state != MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN && state == final_state)) {
        g_task_return_int (task, state);
        g_object_unref (task);
        return;
    }

    /* Otherwise, we'll need to wait for the exact one we want */
    ctx = g_new0 (WaitForPacketServiceStateContext, 1);
    ctx->final_state = final_state;

    g_task_set_task_data (task, ctx, g_free);

    /* Want to get notified when packet service state changes */
    ctx->packet_service_state_changed_id = g_signal_connect (self,
                                                             "notify::" MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE,
                                                             G_CALLBACK (packet_service_state_changed),
                                                             task);
    /* But we don't want to wait forever */
    ctx->packet_service_state_changed_wait_id = g_timeout_add_seconds (10,
                                                                       (GSourceFunc)packet_service_state_changed_wait_expired,
                                                                       task);
}

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

void
mm_iface_modem_3gpp_bind_simple_status (MMIfaceModem3gpp *self,
                                        MMSimpleStatus *status)
{
    MmGdbusModem3gpp *skeleton;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &skeleton,
                  NULL);
    if (!skeleton)
        return;

    g_object_bind_property (skeleton, "registration-state",
                            status, MM_SIMPLE_PROPERTY_3GPP_REGISTRATION_STATE,
                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

    g_object_bind_property (skeleton, "operator-code",
                            status, MM_SIMPLE_PROPERTY_3GPP_OPERATOR_CODE,
                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

    g_object_bind_property (skeleton, "operator-name",
                            status, MM_SIMPLE_PROPERTY_3GPP_OPERATOR_NAME,
                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

    g_object_unref (skeleton);
}

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

#define REG_STATE_IS_REGISTERED(state)                                    \
    (state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME ||                    \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||                 \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_SMS_ONLY ||           \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY ||        \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_CSFB_NOT_PREFERRED || \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED)

#define REG_STATE_IS_UNKNOWN_IDLE_DENIED(state)                           \
    (state == MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN ||                 \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||                    \
     state == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED)

static MMModem3gppPacketServiceState
get_consolidated_packet_service_state (MMIfaceModem3gpp *self)
{
    Private *priv;

    priv = get_private (self);

    /* If registered in any of PS, EPS or 5GS, then packet service domain is
     * implicitly attached. */
    if (REG_STATE_IS_REGISTERED (priv->state_ps) ||
        REG_STATE_IS_REGISTERED (priv->state_eps) ||
        REG_STATE_IS_REGISTERED (priv->state_5gs))
        return MM_MODEM_3GPP_PACKET_SERVICE_STATE_ATTACHED;

    if (REG_STATE_IS_REGISTERED (priv->state_cs))
        return MM_MODEM_3GPP_PACKET_SERVICE_STATE_DETACHED;

    /* If not registered in any of CS, PS, EPS or 5GS, then packet service
     * domain is detached. */
    return MM_MODEM_3GPP_PACKET_SERVICE_STATE_DETACHED;
}

static MMModem3gppRegistrationState
get_consolidated_reg_state (MMIfaceModem3gpp *self)
{
    Private *priv;

    priv = get_private (self);

    /* Some devices (Blackberries for example) will respond to +CGREG, but
     * return ERROR for +CREG, probably because their firmware is just stupid.
     * So here we prefer the +CREG response, but if we never got a successful
     * +CREG response, we'll take +CGREG instead.
     */
    if (priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_HOME || priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
        return priv->state_cs;
    if (priv->state_ps == MM_MODEM_3GPP_REGISTRATION_STATE_HOME || priv->state_ps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
        return priv->state_ps;
    if (priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_HOME || priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
        return priv->state_eps;
    if (priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_HOME || priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING)
        return priv->state_5gs;

    /* Searching? */
    if (priv->state_cs  == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ||
        priv->state_ps  == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ||
        priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ||
        priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING)
        return MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING;

    /* If at least one state is DENIED and the others are UNKNOWN or IDLE, use DENIED */
    if ((priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED ||
         priv->state_ps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED ||
         priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED ||
         priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED) &&
        REG_STATE_IS_UNKNOWN_IDLE_DENIED (priv->state_cs) &&
        REG_STATE_IS_UNKNOWN_IDLE_DENIED (priv->state_ps) &&
        REG_STATE_IS_UNKNOWN_IDLE_DENIED (priv->state_eps) &&
        REG_STATE_IS_UNKNOWN_IDLE_DENIED (priv->state_5gs))
        return MM_MODEM_3GPP_REGISTRATION_STATE_DENIED;

    /* Emergency services? */
    if (priv->state_cs  == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY ||
        priv->state_ps  == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY ||
        priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY ||
        priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY)
        return MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY;

    /* Support for additional registration states reported when on LTE/5GNR.
     *
     * For example, we may see the modem registered in LTE (EPS==HOME), and we
     * may get "SMS only" reported for CS.
     *
     * We give these states a very low priority w.r.t. the other ones as they
     * are really likely never used (i.e. we would get as consolidated the LTE
     * registration state, not the CS fall back state).
     *
     * We also warn in that case, because ideally we should always report the
     * LTE registration state first, not this one.
     */
    if (priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_SMS_ONLY ||
        priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY ||
        priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_HOME_CSFB_NOT_PREFERRED ||
        priv->state_cs == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED) {
        mm_obj_warn (self, "3GPP CSFB registration state is consolidated: %s",
                     mm_modem_3gpp_registration_state_get_string (priv->state_cs));
        return priv->state_cs;
    }

    /* Idle? */
    if (priv->state_cs  == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||
        priv->state_ps  == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||
        priv->state_eps == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE ||
        priv->state_5gs == MM_MODEM_3GPP_REGISTRATION_STATE_IDLE)
        return MM_MODEM_3GPP_REGISTRATION_STATE_IDLE;

    return MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
}

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

typedef struct {
    MMIfaceModem3gpp *self;
    MmGdbusModem3gpp *skeleton;
    GCancellable     *cancellable;
    gchar            *operator_id;
    GTimer           *timer;
    guint             max_registration_time;
} RegisterInNetworkContext;

static void
register_in_network_context_free (RegisterInNetworkContext *ctx)
{
    if (ctx->timer)
        g_timer_destroy (ctx->timer);

    if (ctx->cancellable) {
        Private *priv;

        /* Clear our cancellable if still around */
        priv = get_private (ctx->self);
        if (priv->pending_registration_cancellable == ctx->cancellable)
            g_clear_object (&priv->pending_registration_cancellable);
        g_object_unref (ctx->cancellable);
    }

    g_free (ctx->operator_id);
    if (ctx->skeleton)
        g_object_unref (ctx->skeleton);
    g_object_unref (ctx->self);
    g_slice_free (RegisterInNetworkContext, ctx);
}

static void
register_in_network_context_complete_failed (GTask  *task,
                                             GError *error)
{
    RegisterInNetworkContext *ctx;

    ctx = g_task_get_task_data (task);

    mm_iface_modem_3gpp_update_cs_registration_state      (ctx->self, MM_MODEM_3GPP_REGISTRATION_STATE_IDLE, TRUE);
    mm_iface_modem_3gpp_update_ps_registration_state      (ctx->self, MM_MODEM_3GPP_REGISTRATION_STATE_IDLE, TRUE);
    mm_iface_modem_3gpp_update_eps_registration_state     (ctx->self, MM_MODEM_3GPP_REGISTRATION_STATE_IDLE, TRUE);
    mm_iface_modem_3gpp_update_5gs_registration_state     (ctx->self, MM_MODEM_3GPP_REGISTRATION_STATE_IDLE, TRUE);
    mm_iface_modem_3gpp_apply_deferred_registration_state (ctx->self);

    mm_iface_modem_3gpp_update_access_technologies (ctx->self, MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN);
    mm_iface_modem_3gpp_update_location            (ctx->self, 0, 0, 0);

    g_task_return_error (task, error);
    g_object_unref (task);
}

gboolean
mm_iface_modem_3gpp_register_in_network_finish (MMIfaceModem3gpp  *self,
                                                GAsyncResult      *res,
                                                GError           **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static gboolean run_registration_checks (GTask *task);

static void
run_registration_checks_ready (MMIfaceModem3gpp *self,
                               GAsyncResult     *res,
                               GTask            *task)
{
    RegisterInNetworkContext     *ctx;
    GError                       *error = NULL;
    MMModem3gppRegistrationState  current_registration_state;

    ctx = g_task_get_task_data (task);

    mm_iface_modem_3gpp_run_registration_checks_finish (MM_IFACE_MODEM_3GPP (self), res, &error);
    if (error) {
        mm_obj_dbg (self, "3GPP registration check failed: %s", error->message);
        register_in_network_context_complete_failed (task, error);
        return;
    }

    current_registration_state = get_consolidated_reg_state (ctx->self);

    /* If we got a final state and it's denied, we can assume the registration is
     * finished */
    if (current_registration_state == MM_MODEM_3GPP_REGISTRATION_STATE_DENIED) {
        mm_obj_dbg (self, "registration denied");
        register_in_network_context_complete_failed (
            task,
            mm_mobile_equipment_error_for_code (MM_MOBILE_EQUIPMENT_ERROR_NETWORK_NOT_ALLOWED, self));
        return;
    }

    /* If we got registered, end registration checks */
    if (REG_STATE_IS_REGISTERED (current_registration_state)) {
        /* Request immediate access tech and signal update: we may have changed
         * from home to roaming or viceversa, both registered states, so there
         * wouldn't be an explicit refresh triggered from the modem interface as
         * the modem never got un-registered during the sequence. */
        mm_iface_modem_refresh_signal (MM_IFACE_MODEM (ctx->self));
        mm_obj_dbg (self, "currently registered in a 3GPP network");
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;
    }

    /* Don't spend too much time waiting to get registered */
    if (g_timer_elapsed (ctx->timer, NULL) > ctx->max_registration_time) {
        mm_obj_dbg (self, "3GPP registration check timed out");
        register_in_network_context_complete_failed (
            task,
            mm_mobile_equipment_error_for_code (MM_MOBILE_EQUIPMENT_ERROR_NETWORK_TIMEOUT, self));
        return;
    }

    /* If we're still waiting for automatic registration to complete or
     * fail, check again in a few seconds.
     *
     * This 3s timeout will catch results from automatic registrations as
     * well.
     */
    mm_obj_dbg (self, "not yet registered in a 3GPP network... will recheck soon");
    g_timeout_add_seconds (3, (GSourceFunc)run_registration_checks, task);
}

static gboolean
run_registration_checks (GTask *task)
{
    RegisterInNetworkContext *ctx;

    ctx = g_task_get_task_data (task);

    /* Get fresh registration state */
    mm_iface_modem_3gpp_run_registration_checks (
        ctx->self,
        (GAsyncReadyCallback)run_registration_checks_ready,
        task);
    return G_SOURCE_REMOVE;
}

static void
register_in_network_ready (MMIfaceModem3gpp *self,
                           GAsyncResult     *res,
                           GTask            *task)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->register_in_network_finish (self, res, &error)) {
        /* Propagate error when trying to lock to network */
        register_in_network_context_complete_failed (task, error);
        return;
    }

    /* Now try to gather current registration status until we're registered or
     * the time goes off */
    run_registration_checks (task);
}

void
mm_iface_modem_3gpp_register_in_network (MMIfaceModem3gpp    *self,
                                         const gchar         *operator_id,
                                         gboolean             force_registration,
                                         guint                max_registration_time,
                                         GAsyncReadyCallback  callback,
                                         gpointer             user_data)
{
    RegisterInNetworkContext     *ctx;
    const gchar                  *current_operator_code;
    GError                       *error = NULL;
    GTask                        *task;
    Private                      *priv;
    MMModem3gppRegistrationState  reg_state;

    priv = get_private (self);

    ctx = g_slice_new0 (RegisterInNetworkContext);
    ctx->self = g_object_ref (self);
    ctx->operator_id = (operator_id && operator_id[0]) ? g_strdup (operator_id) : NULL;
    ctx->max_registration_time = max_registration_time;

    task = g_task_new (self, NULL, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)register_in_network_context_free);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &ctx->skeleton,
                  NULL);
    if (!ctx->skeleton) {
        g_task_return_new_error (task,
                                 MM_CORE_ERROR,
                                 MM_CORE_ERROR_FAILED,
                                 "Couldn't get interface skeleton");
        g_object_unref (task);
        return;
    }

    /* Validate input MCC/MNC */
    if (ctx->operator_id && !mm_3gpp_parse_operator_id (ctx->operator_id, NULL, NULL, NULL, &error)) {
        g_assert (error != NULL);
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    /* (Try to) cancel previous registration request */
    if (priv->pending_registration_cancellable) {
        g_cancellable_cancel (priv->pending_registration_cancellable);
        g_clear_object (&priv->pending_registration_cancellable);
    }

    current_operator_code = mm_gdbus_modem3gpp_get_operator_code (ctx->skeleton);
    reg_state = mm_gdbus_modem3gpp_get_registration_state (ctx->skeleton);

    /* Manual registration requested? */
    if (ctx->operator_id) {
        /* If already registered manually with the requested operator, we're done */
        if (!force_registration &&
            (g_strcmp0 (current_operator_code, ctx->operator_id) == 0) &&
            REG_STATE_IS_REGISTERED (reg_state) &&
            priv->manual_registration) {
            mm_obj_dbg (self, "already registered manually in selected network '%s', manual registration not launched...",
                        current_operator_code);
            g_task_return_boolean (task, TRUE);
            g_object_unref (task);
            return;
        }

        /* Manual registration to a new operator required */
        mm_obj_dbg (self, "launching manual network registration (%s)...", ctx->operator_id);
        g_free (priv->manual_registration_operator_id);
        priv->manual_registration_operator_id = g_strdup (ctx->operator_id);
        priv->manual_registration = TRUE;
    }
    /* Automatic registration requested? */
    else {
        /* If the modem is already registered and the last time it was asked
         * automatic registration, we're done */
        if (!force_registration &&
            REG_STATE_IS_REGISTERED (reg_state) &&
            !priv->manual_registration) {
            mm_obj_dbg (self, "already registered automatically in network '%s',"
                        " automatic registration not launched...",
                        current_operator_code ? current_operator_code : "unknown");
            g_task_return_boolean (task, TRUE);
            g_object_unref (task);
            return;
        }

        /* Automatic registration to a new operator requested */
        mm_obj_dbg (self, "launching automatic network registration...");
        g_clear_pointer (&priv->manual_registration_operator_id, g_free);
        priv->manual_registration = FALSE;
    }

    ctx->cancellable = g_cancellable_new ();

    /* Keep an accessible reference to the cancellable, so that we can cancel
     * previous request when needed */
    priv->pending_registration_cancellable = g_object_ref (ctx->cancellable);

    ctx->timer = g_timer_new ();
    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->register_in_network (
        self,
        ctx->operator_id,
        ctx->cancellable,
        (GAsyncReadyCallback)register_in_network_ready,
        task);
}

/*****************************************************************************/
/* Request to reregister using the last settings */

#define REREGISTER_IN_NETWORK_TIMEOUT 120

gboolean
mm_iface_modem_3gpp_reregister_in_network_finish (MMIfaceModem3gpp  *self,
                                                  GAsyncResult      *res,
                                                  GError           **error)
{
    return mm_iface_modem_3gpp_register_in_network_finish (self, res, error);
}

void
mm_iface_modem_3gpp_reregister_in_network (MMIfaceModem3gpp    *self,
                                           GAsyncReadyCallback  callback,
                                           gpointer             user_data)
{
    Private *priv;

    /* Relaunch registration using the last used settings */
    priv = get_private (self);
    mm_iface_modem_3gpp_register_in_network (self,
                                             priv->manual_registration_operator_id,
                                             TRUE, /* if already registered with same settings, force re-registration */
                                             REREGISTER_IN_NETWORK_TIMEOUT,
                                             callback,
                                             user_data);
}


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

typedef struct {
    MmGdbusModem3gpp *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModem3gpp *self;
    gchar *operator_id;
} HandleRegisterContext;

static void
handle_register_context_free (HandleRegisterContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_free (ctx->operator_id);
    g_free (ctx);
}

static void
handle_register_ready (MMIfaceModem3gpp *self,
                       GAsyncResult *res,
                       HandleRegisterContext *ctx)
{
    GError *error = NULL;

    if (!mm_iface_modem_3gpp_register_in_network_finish (self, res, &error))
        g_dbus_method_invocation_take_error (ctx->invocation, error);
    else
        mm_gdbus_modem3gpp_complete_register (ctx->skeleton, ctx->invocation);

    handle_register_context_free (ctx);
}

static void
handle_register_auth_ready (MMBaseModem *self,
                            GAsyncResult *res,
                            HandleRegisterContext *ctx)
{
    MMModemState modem_state;
    GError *error = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_register_context_free (ctx);
        return;
    }

    g_assert (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->register_in_network != NULL);
    g_assert (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->register_in_network_finish != NULL);

    modem_state = MM_MODEM_STATE_UNKNOWN;
    g_object_get (self,
                  MM_IFACE_MODEM_STATE, &modem_state,
                  NULL);

    switch (modem_state) {
    case MM_MODEM_STATE_FAILED:
    case MM_MODEM_STATE_UNKNOWN:
    case MM_MODEM_STATE_INITIALIZING:
    case MM_MODEM_STATE_LOCKED:
    case MM_MODEM_STATE_DISABLED:
    case MM_MODEM_STATE_DISABLING:
    case MM_MODEM_STATE_ENABLING:
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot register modem: "
                                               "not yet enabled");
        handle_register_context_free (ctx);
        return;

    case MM_MODEM_STATE_ENABLED:
    case MM_MODEM_STATE_SEARCHING:
    case MM_MODEM_STATE_REGISTERED:
        mm_iface_modem_3gpp_register_in_network (MM_IFACE_MODEM_3GPP (self),
                                                 ctx->operator_id,
                                                 FALSE, /* if already registered with same settings, do nothing */
                                                 60,
                                                 (GAsyncReadyCallback)handle_register_ready,
                                                 ctx);
        return;

    case MM_MODEM_STATE_DISCONNECTING:
    case MM_MODEM_STATE_CONNECTING:
    case MM_MODEM_STATE_CONNECTED:
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot register modem: "
                                               "modem is connected");
        handle_register_context_free (ctx);
        return;

    default:
        g_assert_not_reached ();
    }
}

static gboolean
handle_register (MmGdbusModem3gpp *skeleton,
                 GDBusMethodInvocation *invocation,
                 const gchar *operator_id,
                 MMIfaceModem3gpp *self)
{
    HandleRegisterContext *ctx;

    ctx = g_new (HandleRegisterContext, 1);
    ctx->skeleton = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self = g_object_ref (self);
    ctx->operator_id = g_strdup (operator_id);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)handle_register_auth_ready,
                             ctx);

    return TRUE;
}

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

typedef struct {
    MmGdbusModem3gpp *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModem3gpp *self;
} HandleScanContext;

static void
handle_scan_context_free (HandleScanContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_free (ctx);
}

static GVariant *
scan_networks_build_result (GList *info_list)
{
    GList *l;
    GVariantBuilder builder;

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));

    for (l = info_list; l; l = g_list_next (l)) {
        MM3gppNetworkInfo *info = l->data;

        if (!info->operator_code) {
            g_warn_if_reached ();
            continue;
        }

        g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}"));

        g_variant_builder_add (&builder, "{sv}",
                               "operator-code", g_variant_new_string (info->operator_code));
        g_variant_builder_add (&builder, "{sv}",
                               "status", g_variant_new_uint32 (info->status));
        g_variant_builder_add (&builder, "{sv}",
                               "access-technology", g_variant_new_uint32 (info->access_tech));
        if (info->operator_long)
            g_variant_builder_add (&builder, "{sv}",
                                   "operator-long", g_variant_new_string (info->operator_long));
        if (info->operator_short)
            g_variant_builder_add (&builder, "{sv}",
                                   "operator-short", g_variant_new_string (info->operator_short));
        g_variant_builder_close (&builder);
    }

    return g_variant_ref_sink (g_variant_builder_end (&builder));
}

static void
handle_scan_ready (MMIfaceModem3gpp *self,
                   GAsyncResult *res,
                   HandleScanContext *ctx)
{
    GError *error = NULL;
    GList *info_list;

    info_list = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->scan_networks_finish (self, res, &error);
    if (error)
        g_dbus_method_invocation_take_error (ctx->invocation, error);
    else {
        GVariant *dict_array;

        dict_array = scan_networks_build_result (info_list);
        mm_gdbus_modem3gpp_complete_scan (ctx->skeleton,
                                          ctx->invocation,
                                          dict_array);
        g_variant_unref (dict_array);
    }

    mm_3gpp_network_info_list_free (info_list);
    handle_scan_context_free (ctx);
}

static void
handle_scan_auth_ready (MMBaseModem *self,
                        GAsyncResult *res,
                        HandleScanContext *ctx)
{
    GError *error = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_scan_context_free (ctx);
        return;
    }

    /* If scanning is not implemented, report an error */
    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->scan_networks ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->scan_networks_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot scan networks: operation not supported");
        handle_scan_context_free (ctx);
        return;
    }

    if (mm_iface_modem_abort_invocation_if_state_not_reached (MM_IFACE_MODEM (self),
                                                              ctx->invocation,
                                                              MM_MODEM_STATE_ENABLED)) {
        handle_scan_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->scan_networks (
        MM_IFACE_MODEM_3GPP (self),
        (GAsyncReadyCallback)handle_scan_ready,
        ctx);
}

static gboolean
handle_scan (MmGdbusModem3gpp *skeleton,
             GDBusMethodInvocation *invocation,
             MMIfaceModem3gpp *self)
{
    HandleScanContext *ctx;

    ctx = g_new (HandleScanContext, 1);
    ctx->skeleton = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self = g_object_ref (self);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)handle_scan_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModem3gpp              *skeleton;
    GDBusMethodInvocation         *invocation;
    MMIfaceModem3gpp              *self;
    MMModem3gppEpsUeModeOperation  mode;
} HandleSetEpsUeModeOperationContext;

static void
handle_set_eps_ue_mode_operation_context_free (HandleSetEpsUeModeOperationContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleSetEpsUeModeOperationContext, ctx);
}

static void
after_set_load_eps_ue_mode_operation_ready (MMIfaceModem3gpp                   *self,
                                            GAsyncResult                       *res,
                                            HandleSetEpsUeModeOperationContext *ctx)
{
    MMModem3gppEpsUeModeOperation  uemode;
    GError                        *error = NULL;

    uemode = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation_finish (self, res, &error);
    if (error) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    if (uemode != ctx->mode) {
        g_dbus_method_invocation_return_error_literal (ctx->invocation,
                                                       MM_CORE_ERROR,
                                                       MM_CORE_ERROR_FAILED,
                                                       "UE mode of operation for EPS wasn't updated");
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    mm_gdbus_modem3gpp_set_eps_ue_mode_operation (ctx->skeleton, uemode);
    mm_gdbus_modem3gpp_complete_set_eps_ue_mode_operation (ctx->skeleton, ctx->invocation);
    handle_set_eps_ue_mode_operation_context_free (ctx);
}

static void
handle_set_eps_ue_mode_operation_ready (MMIfaceModem3gpp                   *self,
                                        GAsyncResult                       *res,
                                        HandleSetEpsUeModeOperationContext *ctx)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_eps_ue_mode_operation_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation_finish) {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation (
            self,
            (GAsyncReadyCallback)after_set_load_eps_ue_mode_operation_ready,
            ctx);
        return;
    }

    /* Assume we're ok */
    mm_gdbus_modem3gpp_complete_set_eps_ue_mode_operation (ctx->skeleton, ctx->invocation);
    handle_set_eps_ue_mode_operation_context_free (ctx);
}

static void
handle_set_eps_ue_mode_operation_auth_ready (MMBaseModem                        *self,
                                             GAsyncResult                       *res,
                                             HandleSetEpsUeModeOperationContext *ctx)
{
    GError *error = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    /* Check if we already are in the requested mode */
    if (mm_gdbus_modem3gpp_get_eps_ue_mode_operation (ctx->skeleton) == ctx->mode) {
        /* Nothing to do */
        mm_gdbus_modem3gpp_complete_set_eps_ue_mode_operation (ctx->skeleton, ctx->invocation);
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    /* If UE mode update is not implemented, report an error */
    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_eps_ue_mode_operation ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_eps_ue_mode_operation_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot set UE mode of operation for EPS: operation not supported");
        handle_set_eps_ue_mode_operation_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_eps_ue_mode_operation (
        MM_IFACE_MODEM_3GPP (self),
        ctx->mode,
        (GAsyncReadyCallback)handle_set_eps_ue_mode_operation_ready,
        ctx);
}

static gboolean
handle_set_eps_ue_mode_operation (MmGdbusModem3gpp      *skeleton,
                                  GDBusMethodInvocation *invocation,
                                  guint                  mode,
                                  MMIfaceModem3gpp      *self)
{
    HandleSetEpsUeModeOperationContext *ctx;

    ctx = g_slice_new (HandleSetEpsUeModeOperationContext);
    ctx->skeleton   = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self       = g_object_ref (self);
    ctx->mode       = mode;

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)handle_set_eps_ue_mode_operation_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModem3gpp      *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModem3gpp      *self;
    GVariant              *dictionary;
    MMBearerProperties    *config;
} HandleSetInitialEpsBearerSettingsContext;

static void
handle_set_initial_eps_bearer_settings_context_free (HandleSetInitialEpsBearerSettingsContext *ctx)
{
    g_clear_object (&ctx->config);
    g_variant_unref (ctx->dictionary);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleSetInitialEpsBearerSettingsContext, ctx);
}

static void
log_initial_eps_bearer_settings (MMIfaceModem3gpp   *self,
                                 MMBearerProperties *properties)
{
    const gchar         *apn;
    MMBearerAllowedAuth  allowed_auth;
    const gchar         *user;
    const gchar         *password;
    MMBearerIpFamily     ip_family;

    apn = mm_bearer_properties_get_apn (properties);
    if (apn)
        mm_obj_dbg (self, "  APN: '%s'", apn);

    allowed_auth = mm_bearer_properties_get_allowed_auth (properties);
    if (allowed_auth != MM_BEARER_ALLOWED_AUTH_UNKNOWN) {
        g_autofree gchar *allowed_auth_str = NULL;

        allowed_auth_str = mm_bearer_allowed_auth_build_string_from_mask (allowed_auth);
        mm_obj_dbg (self, "  allowed auth: '%s'", allowed_auth_str);
    }

    user = mm_bearer_properties_get_user (properties);
    if (user)
        mm_obj_dbg (self, "  user: '%s'", user);

    password = mm_bearer_properties_get_password (properties);
    if (password)
        mm_obj_dbg (self, "  password: '%s'", password);

    ip_family = mm_bearer_properties_get_ip_type (properties);
    if (ip_family != MM_BEARER_IP_FAMILY_NONE) {
        g_autofree gchar *ip_family_str = NULL;

        ip_family_str = mm_bearer_ip_family_build_string_from_mask (ip_family);
        mm_obj_dbg (self, "  ip family: '%s'", ip_family_str);
    }
}

static void
after_set_load_initial_eps_bearer_settings_ready (MMIfaceModem3gpp                         *self,
                                                  GAsyncResult                             *res,
                                                  HandleSetInitialEpsBearerSettingsContext *ctx)
{
    GError             *error = NULL;
    MMBearerProperties *new_config;

    new_config = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings_finish (self, res, &error);
    if (error) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_initial_eps_bearer_settings_context_free (ctx);
        return;
    }

    mm_obj_dbg (self, "Updated initial EPS bearer settings:");
    log_initial_eps_bearer_settings (self, new_config);

    if (!mm_bearer_properties_cmp (new_config, ctx->config, MM_BEARER_PROPERTIES_CMP_FLAGS_EPS)) {
        mm_obj_dbg (self, "Requested initial EPS bearer settings:");
        log_initial_eps_bearer_settings (self, ctx->config);
        g_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                                       "Initial EPS bearer settings were not updated");
    } else {
        GVariant *dictionary;

        dictionary = mm_bearer_properties_get_dictionary (new_config);
        mm_gdbus_modem3gpp_set_initial_eps_bearer_settings (ctx->skeleton, dictionary);
        if (dictionary)
            g_variant_unref (dictionary);
        mm_gdbus_modem3gpp_complete_set_initial_eps_bearer_settings (ctx->skeleton, ctx->invocation);
    }

    handle_set_initial_eps_bearer_settings_context_free (ctx);
    g_object_unref (new_config);
}

static void
set_initial_eps_bearer_settings_ready (MMIfaceModem3gpp                         *self,
                                       GAsyncResult                             *res,
                                       HandleSetInitialEpsBearerSettingsContext *ctx)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_initial_eps_bearer_settings_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_initial_eps_bearer_settings_context_free (ctx);
        return;
    }

    if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings_finish) {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings (
            self,
            (GAsyncReadyCallback)after_set_load_initial_eps_bearer_settings_ready,
            ctx);
        return;
    }

    /* Assume we're ok */
    mm_gdbus_modem3gpp_complete_set_initial_eps_bearer_settings (ctx->skeleton, ctx->invocation);
    handle_set_initial_eps_bearer_settings_context_free (ctx);
}

static void
set_initial_eps_bearer_settings_auth_ready (MMBaseModem                              *self,
                                            GAsyncResult                             *res,
                                            HandleSetInitialEpsBearerSettingsContext *ctx)
{
    GError             *error = NULL;
    MMBearerProperties *old_config = NULL;
    GVariant           *old_dictionary;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_initial_eps_bearer_settings_context_free (ctx);
        return;
    }

    /* If UE mode update is not implemented, report an error */
    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_initial_eps_bearer_settings ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_initial_eps_bearer_settings_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot set initial EPS bearer settings: operation not supported");
        handle_set_initial_eps_bearer_settings_context_free (ctx);
        return;
    }

    ctx->config = mm_bearer_properties_new_from_dictionary (ctx->dictionary, &error);
    if (!ctx->config) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_initial_eps_bearer_settings_context_free (ctx);
        return;
    }

    old_dictionary = mm_gdbus_modem3gpp_get_initial_eps_bearer_settings (ctx->skeleton);
    if (old_dictionary)
        old_config = mm_bearer_properties_new_from_dictionary (old_dictionary, NULL);

    if (old_config && mm_bearer_properties_cmp (ctx->config, old_config, MM_BEARER_PROPERTIES_CMP_FLAGS_EPS)) {
        mm_obj_dbg (self, "Skipping initial eps bearer configuration. Same configuration provided");
        mm_gdbus_modem3gpp_complete_set_initial_eps_bearer_settings (ctx->skeleton, ctx->invocation);
        handle_set_initial_eps_bearer_settings_context_free (ctx);
    } else {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_initial_eps_bearer_settings (
            MM_IFACE_MODEM_3GPP (self),
            ctx->config,
            (GAsyncReadyCallback)set_initial_eps_bearer_settings_ready,
            ctx);
    }

    g_clear_object (&old_config);
}

static gboolean
handle_set_initial_eps_bearer_settings (MmGdbusModem3gpp      *skeleton,
                                        GDBusMethodInvocation *invocation,
                                        GVariant              *dictionary,
                                        MMIfaceModem3gpp      *self)
{
    HandleSetInitialEpsBearerSettingsContext *ctx;

    ctx = g_slice_new0 (HandleSetInitialEpsBearerSettingsContext);
    ctx->skeleton   = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self       = g_object_ref (self);
    ctx->dictionary = g_variant_ref (dictionary);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)set_initial_eps_bearer_settings_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModem3gpp      *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModem3gpp      *self;
    GVariant              *dictionary;
    MMModem3gppFacility    facility;
    guint8                 slot;
    gchar                 *control_key;
} HandleDisableFacilityLockContext;

static void
handle_disable_facility_lock_context_free (HandleDisableFacilityLockContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_variant_unref (ctx->dictionary);
    g_free (ctx->control_key);
    g_free (ctx);
}

static void
update_lock_info_ready (MMIfaceModem                     *modem,
                        GAsyncResult                     *res,
                        HandleDisableFacilityLockContext *ctx)
{
    GError      *error = NULL;

    mm_iface_modem_update_lock_info_finish (modem, res, &error);
    if (error) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    mm_gdbus_modem3gpp_complete_disable_facility_lock (ctx->skeleton, ctx->invocation);
    handle_disable_facility_lock_context_free (ctx);
}

static void
handle_disable_facility_lock_ready (MMIfaceModem3gpp                 *self,
                                    GAsyncResult                     *res,
                                    HandleDisableFacilityLockContext *ctx)
{
    MMModem3gppFacility  facilities;
    GError              *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_facility_lock_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    /* Update the Enabled Facility Locks property in the DBus interface */
    facilities = mm_gdbus_modem3gpp_get_enabled_facility_locks (ctx->skeleton);
    facilities &= ~ctx->facility;
    mm_gdbus_modem3gpp_set_enabled_facility_locks (ctx->skeleton, facilities);

    /* Recheck lock status after unlock code has been sent  */
    mm_iface_modem_update_lock_info (MM_IFACE_MODEM (self),
                                     MM_MODEM_LOCK_UNKNOWN, /* ask */
                                     (GAsyncReadyCallback)update_lock_info_ready,
                                     ctx);
}

static void
disable_facility_lock_auth_ready (MMBaseModem                      *self,
                                  GAsyncResult                     *res,
                                  HandleDisableFacilityLockContext *ctx)
{
    GError *error = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    /* If disable facility locks is not implemented, report an error */
    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_facility_lock ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_facility_lock_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot disable facility locks: operation not supported");
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    /* Parse properties dictionary */
    if (!g_variant_is_of_type (ctx->dictionary, G_VARIANT_TYPE ("(us)"))) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot disable facility locks: invalid parameters");
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    /* Only modems with single slot or single configuration for all slots are supported */
    ctx->slot = 1;

    g_variant_get (ctx->dictionary, "(us)", &ctx->facility, &ctx->control_key);

    /* Only four facility locks can be disabled:
     * - MM_MODEM_3GPP_FACILITY_NET_PERS (network personalization)
     * - MM_MODEM_3GPP_FACILITY_NET_SUB_PERS (network subset personalization)
     * - MM_MODEM_3GPP_FACILITY_PROVIDER_PERS (service provider personalization)
     * - MM_MODEM_3GPP_FACILITY_CORP_PERS (corporate personalization)
     */
    if (ctx->facility != (ctx->facility & (MM_MODEM_3GPP_FACILITY_NET_PERS |
                                           MM_MODEM_3GPP_FACILITY_NET_SUB_PERS |
                                           MM_MODEM_3GPP_FACILITY_PROVIDER_PERS |
                                           MM_MODEM_3GPP_FACILITY_CORP_PERS))) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Invalid type of facility lock to disable or empty key");
        handle_disable_facility_lock_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_facility_lock (
        MM_IFACE_MODEM_3GPP (self),
        ctx->facility,
        ctx->slot,
        ctx->control_key,
        (GAsyncReadyCallback)handle_disable_facility_lock_ready,
        ctx);
}

static gboolean
handle_disable_facility_lock (MmGdbusModem3gpp      *skeleton,
                              GDBusMethodInvocation *invocation,
                              GVariant              *dictionary,
                              MMIfaceModem3gpp      *self)
{
    HandleDisableFacilityLockContext *ctx;

    ctx = g_new0 (HandleDisableFacilityLockContext, 1);
    ctx->skeleton   = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self       = g_object_ref (self);
    ctx->dictionary = g_variant_ref (dictionary);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)disable_facility_lock_auth_ready,
                             ctx);
    return TRUE;
}

/*****************************************************************************/
/* Set Packet Service State */

typedef struct {
    MMIfaceModem3gpp               *self;
    MmGdbusModem3gpp               *skeleton;
    GDBusMethodInvocation          *invocation;
    MMModem3gppPacketServiceState   packet_service_state;
} HandlePacketServiceStateContext;

static void
handle_set_packet_service_state_context_free (HandlePacketServiceStateContext *ctx)
{
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->self);
    g_slice_free (HandlePacketServiceStateContext,ctx);
}

static void
set_packet_service_state_ready (MMIfaceModem3gpp                *self,
                                GAsyncResult                    *res,
                                HandlePacketServiceStateContext *ctx)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_packet_service_state_finish (self, res, &error))
        g_dbus_method_invocation_take_error (ctx->invocation, error);
    else
        mm_gdbus_modem3gpp_complete_set_packet_service_state (ctx->skeleton, ctx->invocation);
    handle_set_packet_service_state_context_free (ctx);
}

static void
set_packet_service_state_auth_ready (MMBaseModem                     *self,
                                     GAsyncResult                    *res,
                                     HandlePacketServiceStateContext *ctx)
{
    GError *error = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_packet_service_state_context_free (ctx);
        return;
    }

    if (mm_iface_modem_abort_invocation_if_state_not_reached (MM_IFACE_MODEM (self),
                                                              ctx->invocation,
                                                              MM_MODEM_STATE_ENABLED)) {
        handle_set_packet_service_state_context_free (ctx);
        return;
    }

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (ctx->self)->set_packet_service_state ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (ctx->self)->set_packet_service_state_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                               "Explicit packet service attach/detach operation not supported");
        handle_set_packet_service_state_context_free (ctx);
        return;
    }

    if ((ctx->packet_service_state != MM_MODEM_3GPP_PACKET_SERVICE_STATE_ATTACHED) &&
        (ctx->packet_service_state != MM_MODEM_3GPP_PACKET_SERVICE_STATE_DETACHED)) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
                                               "Invalid packet service state requested");
        handle_set_packet_service_state_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_packet_service_state (ctx->self,
                                                                        ctx->packet_service_state,
                                                                        (GAsyncReadyCallback)set_packet_service_state_ready,
                                                                        ctx);
}

static gboolean
handle_set_packet_service_state (MmGdbusModem3gpp              *skeleton,
                                 GDBusMethodInvocation         *invocation,
                                 MMModem3gppPacketServiceState  packet_service_state,
                                 MMIfaceModem3gpp              *self)
{
    HandlePacketServiceStateContext *ctx;

    ctx = g_slice_new (HandlePacketServiceStateContext);
    ctx->skeleton = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self = g_object_ref (self);
    ctx->packet_service_state = packet_service_state;

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)set_packet_service_state_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModem3gpp           *skeleton;
    GDBusMethodInvocation      *invocation;
    MMIfaceModem3gpp           *self;
    GVariant                   *dictionary;
    MMNr5gRegistrationSettings *settings;
} HandleSetNr5gRegistrationSettingsContext;

static void
handle_set_nr5g_registration_settings_context_free (HandleSetNr5gRegistrationSettingsContext *ctx)
{
    g_clear_object (&ctx->settings);
    g_variant_unref (ctx->dictionary);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleSetNr5gRegistrationSettingsContext, ctx);
}

static void
after_set_load_nr5g_registration_settings_ready (MMIfaceModem3gpp                         *self,
                                                 GAsyncResult                             *res,
                                                 HandleSetNr5gRegistrationSettingsContext *ctx)
{
    GError                                *error = NULL;
    g_autoptr(MMNr5gRegistrationSettings)  new_settings = NULL;

    new_settings = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings_finish (self, res, &error);
    if (error) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_nr5g_registration_settings_context_free (ctx);
        return;
    }

    mm_obj_dbg (self, "Updated 5GNR registration settings");

    if (!mm_nr5g_registration_settings_cmp (new_settings, ctx->settings)) {
        g_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                                                       "5GNR registration settings were not updated");
    } else {
        g_autoptr(GVariant) dictionary = NULL;

        dictionary = mm_nr5g_registration_settings_get_dictionary (new_settings);
        mm_gdbus_modem3gpp_set_nr5g_registration_settings (ctx->skeleton, dictionary);
        mm_gdbus_modem3gpp_complete_set_nr5g_registration_settings (ctx->skeleton, ctx->invocation);
    }

    handle_set_nr5g_registration_settings_context_free (ctx);
}

static void
set_nr5g_registration_settings_ready (MMIfaceModem3gpp                         *self,
                                      GAsyncResult                             *res,
                                      HandleSetNr5gRegistrationSettingsContext *ctx)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_nr5g_registration_settings_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_nr5g_registration_settings_context_free (ctx);
        return;
    }

    if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings_finish) {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings (
            self,
            (GAsyncReadyCallback)after_set_load_nr5g_registration_settings_ready,
            ctx);
        return;
    }

    /* Assume we're ok */
    mm_gdbus_modem3gpp_complete_set_nr5g_registration_settings (ctx->skeleton, ctx->invocation);
    handle_set_nr5g_registration_settings_context_free (ctx);
}

static void
set_nr5g_registration_settings_auth_ready (MMBaseModem                              *self,
                                           GAsyncResult                             *res,
                                           HandleSetNr5gRegistrationSettingsContext *ctx)
{
    GError                                *error = NULL;
    GVariant                              *old_dictionary;
    g_autoptr(MMNr5gRegistrationSettings)  old_settings = NULL;

    if (!mm_base_modem_authorize_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_nr5g_registration_settings_context_free (ctx);
        return;
    }

    /* If 5GNR registration settings update is not implemented, report an error */
    if (!MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_nr5g_registration_settings ||
        !MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_nr5g_registration_settings_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot set 5GNR registration settings: operation not supported");
        handle_set_nr5g_registration_settings_context_free (ctx);
        return;
    }

    ctx->settings = mm_nr5g_registration_settings_new_from_dictionary (ctx->dictionary, &error);
    if (!ctx->settings) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_set_nr5g_registration_settings_context_free (ctx);
        return;
    }

    old_dictionary = mm_gdbus_modem3gpp_get_nr5g_registration_settings (ctx->skeleton);
    if (old_dictionary)
        old_settings = mm_nr5g_registration_settings_new_from_dictionary (old_dictionary, NULL);

    if (old_settings && mm_nr5g_registration_settings_cmp (ctx->settings, old_settings)) {
        mm_obj_dbg (self, "Skipping 5GNR registration settings. Same configuration provided");
        mm_gdbus_modem3gpp_complete_set_nr5g_registration_settings (ctx->skeleton, ctx->invocation);
        handle_set_nr5g_registration_settings_context_free (ctx);
    } else {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->set_nr5g_registration_settings (
            MM_IFACE_MODEM_3GPP (self),
            ctx->settings,
            (GAsyncReadyCallback)set_nr5g_registration_settings_ready,
            ctx);
    }
}

static gboolean
handle_set_nr5g_registration_settings (MmGdbusModem3gpp      *skeleton,
                                       GDBusMethodInvocation *invocation,
                                       GVariant              *dictionary,
                                       MMIfaceModem3gpp      *self)
{
    HandleSetNr5gRegistrationSettingsContext *ctx;

    ctx = g_slice_new0 (HandleSetNr5gRegistrationSettingsContext);
    ctx->skeleton   = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self       = g_object_ref (self);
    ctx->dictionary = g_variant_ref (dictionary);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_DEVICE_CONTROL,
                             (GAsyncReadyCallback)set_nr5g_registration_settings_auth_ready,
                             ctx);
    return TRUE;
}

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

gboolean
mm_iface_modem_3gpp_run_registration_checks_finish (MMIfaceModem3gpp  *self,
                                                    GAsyncResult      *res,
                                                    GError           **error)
{
    g_assert (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->run_registration_checks_finish != NULL);
    return MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->run_registration_checks_finish (self, res, error);
}

void
mm_iface_modem_3gpp_run_registration_checks (MMIfaceModem3gpp    *self,
                                             GAsyncReadyCallback  callback,
                                             gpointer             user_data)
{
    gboolean is_cs_supported;
    gboolean is_ps_supported;
    gboolean is_eps_supported;
    gboolean is_5gs_supported;

    g_assert (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->run_registration_checks != NULL);

    is_cs_supported  = get_cs_network_supported  (self);
    is_ps_supported  = get_ps_network_supported  (self);
    is_eps_supported = get_eps_network_supported (self);
    is_5gs_supported = get_5gs_network_supported (self);

    mm_obj_dbg (self, "running registration checks (CS: '%s', PS: '%s', EPS: '%s', 5GS: '%s')",
                is_cs_supported ? "yes" : "no",
                is_ps_supported ? "yes" : "no",
                is_eps_supported ? "yes" : "no",
                is_5gs_supported ? "yes" : "no");

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->run_registration_checks (self,
                                                                       is_cs_supported,
                                                                       is_ps_supported,
                                                                       is_eps_supported,
                                                                       is_5gs_supported,
                                                                       callback,
                                                                       user_data);
}

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

typedef struct {
    MmGdbusModem3gpp *skeleton;
    gboolean operator_code_loaded;
    gboolean operator_name_loaded;
} ReloadCurrentRegistrationInfoContext;

static void
reload_current_registration_info_context_free (ReloadCurrentRegistrationInfoContext *ctx)
{
    if (ctx->skeleton)
        g_object_unref (ctx->skeleton);
    g_slice_free (ReloadCurrentRegistrationInfoContext, ctx);
}

gboolean
mm_iface_modem_3gpp_reload_current_registration_info_finish (MMIfaceModem3gpp *self,
                                                             GAsyncResult *res,
                                                             GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void reload_current_registration_info_context_step (GTask *task);

static void
load_operator_name_ready (MMIfaceModem3gpp *self,
                          GAsyncResult *res,
                          GTask *task)
{
    ReloadCurrentRegistrationInfoContext *ctx;
    GError *error = NULL;
    gchar *str;

    ctx = g_task_get_task_data (task);

    str = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_name_finish (self, res, &error);
    if (error) {
        mm_obj_warn (self, "couldn't load operator name: %s", error->message);
        g_error_free (error);
    }

    if (ctx->skeleton)
        mm_gdbus_modem3gpp_set_operator_name (ctx->skeleton, str);
    g_free (str);

    ctx->operator_name_loaded = TRUE;
    reload_current_registration_info_context_step (task);
}

static void
load_operator_code_ready (MMIfaceModem3gpp *self,
                          GAsyncResult *res,
                          GTask *task)
{
    ReloadCurrentRegistrationInfoContext *ctx;
    GError *error = NULL;
    gchar *str;

    ctx = g_task_get_task_data (task);

    str = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_code_finish (self, res, &error);
    if (error) {
        mm_obj_warn (self, "couldn't load operator code: %s", error->message);
    } else if (!mm_3gpp_parse_operator_id (str, NULL, NULL, NULL, &error)) {
        mm_obj_dbg (self, "unexpected operator code string '%s': %s", str, error->message);
        g_clear_pointer (&str, g_free);
    }
    g_clear_error (&error);

    if (ctx->skeleton)
        mm_gdbus_modem3gpp_set_operator_code (ctx->skeleton, str);

    /* If we also implement the location interface, update the 3GPP location */
    if (str && MM_IS_IFACE_MODEM_LOCATION (self))
        mm_iface_modem_location_3gpp_update_operator_code (MM_IFACE_MODEM_LOCATION (self), str);

    g_free (str);

    ctx->operator_code_loaded = TRUE;
    reload_current_registration_info_context_step (task);
}

static void
reload_current_registration_info_context_step (GTask *task)
{
    MMIfaceModem3gpp *self;
    ReloadCurrentRegistrationInfoContext *ctx;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    if (!ctx->operator_code_loaded) {
        /* Launch operator code update */
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_code (
            self,
            (GAsyncReadyCallback)load_operator_code_ready,
            task);
        return;
    }

    if (!ctx->operator_name_loaded) {
        /* Launch operator name update */
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_name (
            self,
            (GAsyncReadyCallback)load_operator_name_ready,
            task);
        return;
    }

    /* If all are loaded, all done */
    g_task_return_boolean (task, TRUE);
    g_object_unref (task);
}

void
mm_iface_modem_3gpp_reload_current_registration_info (MMIfaceModem3gpp *self,
                                                      GAsyncReadyCallback callback,
                                                      gpointer user_data)
{
    ReloadCurrentRegistrationInfoContext *ctx;
    GTask *task;

    ctx = g_slice_new0 (ReloadCurrentRegistrationInfoContext);

    task = g_task_new (self, NULL, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)reload_current_registration_info_context_free);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &ctx->skeleton,
                  NULL);
    if (!ctx->skeleton) {
        g_task_return_new_error (task,
                                 MM_CORE_ERROR,
                                 MM_CORE_ERROR_FAILED,
                                 "Couldn't get interface skeleton");
        g_object_unref (task);
        return;
    }

    ctx->operator_code_loaded = !(MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_code &&
                                  MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_code_finish);
    if (ctx->operator_code_loaded) {
        mm_gdbus_modem3gpp_set_operator_code (ctx->skeleton, NULL);
        if (MM_IS_IFACE_MODEM_LOCATION (self))
            mm_iface_modem_location_3gpp_update_operator_code (MM_IFACE_MODEM_LOCATION (self), NULL);
    }

    ctx->operator_name_loaded = !(MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_name &&
                                  MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_operator_name_finish);
    if (ctx->operator_name_loaded)
        mm_gdbus_modem3gpp_set_operator_name (ctx->skeleton, NULL);

    reload_current_registration_info_context_step (task);
}

void
mm_iface_modem_3gpp_clear_current_operator (MMIfaceModem3gpp *self)
{
    MmGdbusModem3gpp *skeleton = NULL;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &skeleton,
                  NULL);
    if (!skeleton)
        return;

    mm_gdbus_modem3gpp_set_operator_code (skeleton, NULL);
    mm_gdbus_modem3gpp_set_operator_name (skeleton, NULL);
    if (MM_IS_IFACE_MODEM_LOCATION (self))
        mm_iface_modem_location_3gpp_update_operator_code (MM_IFACE_MODEM_LOCATION (self), NULL);
}

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

void
mm_iface_modem_3gpp_update_access_technologies (MMIfaceModem3gpp        *self,
                                                MMModemAccessTechnology  access_tech)
{
    Private                      *priv;
    MMModem3gppRegistrationState  state;

    priv = get_private (self);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_REGISTRATION_STATE, &state,
                  NULL);

    /* Even if registration state didn't change, report access technology,
     * but only if something valid to report */
    if (REG_STATE_IS_REGISTERED (state) || priv->reloading_registration_info) {
        if (access_tech != MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN)
            mm_iface_modem_update_access_technologies (MM_IFACE_MODEM (self),
                                                       access_tech,
                                                       MM_IFACE_MODEM_3GPP_ALL_ACCESS_TECHNOLOGIES_MASK);
    } else
        mm_iface_modem_update_access_technologies (MM_IFACE_MODEM (self),
                                                   MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN,
                                                   MM_IFACE_MODEM_3GPP_ALL_ACCESS_TECHNOLOGIES_MASK);
}

void
mm_iface_modem_3gpp_update_location (MMIfaceModem3gpp *self,
                                     gulong            location_area_code,
                                     gulong            tracking_area_code,
                                     gulong            cell_id)
{
    Private                      *priv;
    MMModem3gppRegistrationState  state;

    priv = get_private (self);

    if (!MM_IS_IFACE_MODEM_LOCATION (self))
        return;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_REGISTRATION_STATE, &state,
                  NULL);

    /* Even if registration state didn't change, report access technology or
     * location updates, but only if something valid to report. For the case
     * where we're registering (loading current registration info after a state
     * change to registered), we also allow LAC/CID updates. */
    if (REG_STATE_IS_REGISTERED (state) || priv->reloading_registration_info) {
        if (location_area_code || tracking_area_code || cell_id)
            mm_iface_modem_location_3gpp_update_lac_tac_ci (MM_IFACE_MODEM_LOCATION (self),
                                                            location_area_code,
                                                            tracking_area_code,
                                                            cell_id);
    } else
        mm_iface_modem_location_3gpp_clear (MM_IFACE_MODEM_LOCATION (self));
}

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

static void
update_registration_reload_current_registration_info_ready (MMIfaceModem3gpp *self,
                                                            GAsyncResult     *res,
                                                            gpointer          user_data)
{
    Private                      *priv;
    MMModem3gppRegistrationState  new_state;

    priv = get_private (self);

    new_state = GPOINTER_TO_UINT (user_data);

    mm_obj_info (self, "3GPP registration state changed (registering -> %s)",
                 mm_modem_3gpp_registration_state_get_string (new_state));
    mm_obj_dbg (self, "consolidated registration state: cs '%s', ps '%s', eps '%s', 5gs '%s' --> '%s'",
                mm_modem_3gpp_registration_state_get_string (priv->state_cs),
                mm_modem_3gpp_registration_state_get_string (priv->state_ps),
                mm_modem_3gpp_registration_state_get_string (priv->state_eps),
                mm_modem_3gpp_registration_state_get_string (priv->state_5gs),
                mm_modem_3gpp_registration_state_get_string (new_state));

    /* The properties in the interface are bound to the properties
     * in the skeleton, so just updating here is enough */
    g_object_set (self,
                  MM_IFACE_MODEM_3GPP_REGISTRATION_STATE,   new_state,
                  MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE, get_consolidated_packet_service_state (self),
                  NULL);

    mm_iface_modem_update_subsystem_state (MM_IFACE_MODEM (self),
                                           SUBSYSTEM_3GPP,
                                           MM_MODEM_STATE_REGISTERED,
                                           MM_MODEM_STATE_CHANGE_REASON_UNKNOWN);

    priv->reloading_registration_info = FALSE;
}

static void
update_non_registered_state (MMIfaceModem3gpp             *self,
                             MMModem3gppRegistrationState  old_state,
                             MMModem3gppRegistrationState  new_state)
{
    /* Not registered neither in home nor roaming network */
    mm_iface_modem_3gpp_clear_current_operator (self);

    /* The property in the interface is bound to the property
     * in the skeleton, so just updating here is enough */
    g_object_set (self,
                  MM_IFACE_MODEM_3GPP_REGISTRATION_STATE,   new_state,
                  MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE, MM_MODEM_3GPP_PACKET_SERVICE_STATE_DETACHED,
                  NULL);

    mm_iface_modem_update_subsystem_state (
        MM_IFACE_MODEM (self),
        SUBSYSTEM_3GPP,
        (new_state == MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING ?
         MM_MODEM_STATE_SEARCHING :
         MM_MODEM_STATE_ENABLED),
        MM_MODEM_STATE_CHANGE_REASON_UNKNOWN);
}

static void
update_registration_state (MMIfaceModem3gpp             *self,
                           MMModem3gppRegistrationState  new_state)
{
    Private                       *priv;
    MMModem3gppRegistrationState   old_state = MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN;
    MMModem3gppPacketServiceState  old_packet_service_state = MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN;

    priv = get_private (self);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_REGISTRATION_STATE,   &old_state,
                  MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE, &old_packet_service_state,
                  NULL);

    /* Only set new state if different */
    if (new_state == old_state && old_packet_service_state == get_consolidated_packet_service_state (self))
        return;

    if (REG_STATE_IS_REGISTERED (new_state)) {
        MMModemState modem_state;

        /* If already reloading registration info, skip it */
        if (priv->reloading_registration_info)
            return;

        /* If the modem isn't already enabled, this registration state update
         * could be due to a previously scheduled initial registration check
         * when the modem was being enabled.  We need to ignore it as otherwise
         * it may cause an incorrect transition of the registration state and
         * modem state when the modem is being disabled or still going through
         * enable steps */
        modem_state = MM_MODEM_STATE_UNKNOWN;
        g_object_get (self,
                      MM_IFACE_MODEM_STATE, &modem_state,
                      NULL);
        if (modem_state < MM_MODEM_STATE_ENABLED) {
            mm_obj_dbg (self, "3GPP registration state change ignored as modem isn't enabled");
            return;
        }

        mm_obj_info (self, "3GPP registration state changed (%s -> registering)",
                     mm_modem_3gpp_registration_state_get_string (old_state));

        /* Reload current registration info. ONLY update the state to REGISTERED
         * after having loaded operator code/name/subscription state */
        priv->reloading_registration_info = TRUE;
        mm_iface_modem_3gpp_reload_current_registration_info (
            self,
            (GAsyncReadyCallback)update_registration_reload_current_registration_info_ready,
            GUINT_TO_POINTER (new_state));
        return;
    }

    mm_obj_info (self, "3GPP registration state changed (%s -> %s)",
                 mm_modem_3gpp_registration_state_get_string (old_state),
                 mm_modem_3gpp_registration_state_get_string (new_state));
    mm_obj_dbg (self, "consolidated registration state: cs '%s', ps '%s', eps '%s', 5gs '%s' --> '%s'",
                mm_modem_3gpp_registration_state_get_string (priv->state_cs),
                mm_modem_3gpp_registration_state_get_string (priv->state_ps),
                mm_modem_3gpp_registration_state_get_string (priv->state_eps),
                mm_modem_3gpp_registration_state_get_string (priv->state_5gs),
                mm_modem_3gpp_registration_state_get_string (new_state));

    update_non_registered_state (self, old_state, new_state);
}

#define UPDATE_REGISTRATION_STATE(domain)                                                             \
    void                                                                                              \
    mm_iface_modem_3gpp_update_##domain##_registration_state (MMIfaceModem3gpp             *self,     \
                                                              MMModem3gppRegistrationState  state,    \
                                                              gboolean                      deferred) \
    {                                                                                                 \
        Private  *priv;                                                                               \
                                                                                                      \
        if (!get_##domain##_network_supported (self))                                                 \
            return;                                                                                   \
                                                                                                      \
        priv = get_private (self);                                                                    \
        priv->state_##domain = state;                                                                 \
                                                                                                      \
        if (!deferred)                                                                                \
            mm_iface_modem_3gpp_apply_deferred_registration_state (self);                             \
    }

UPDATE_REGISTRATION_STATE (cs)
UPDATE_REGISTRATION_STATE (ps)
UPDATE_REGISTRATION_STATE (eps)
UPDATE_REGISTRATION_STATE (5gs)

void
mm_iface_modem_3gpp_apply_deferred_registration_state (MMIfaceModem3gpp *self)
{
    update_registration_state (self, get_consolidated_reg_state (self));
}

/*****************************************************************************/
/* Periodic registration checks */

#define REGISTRATION_CHECK_TIMEOUT_SEC 30

static void
periodic_registration_checks_ready (MMIfaceModem3gpp *self,
                                    GAsyncResult     *res)
{
    Private *priv;
    GError  *error = NULL;

    priv = get_private (self);

    mm_iface_modem_3gpp_run_registration_checks_finish (self, res, &error);
    if (error) {
        mm_obj_dbg (self, "couldn't refresh 3GPP registration status: %s", error->message);
        g_error_free (error);
    }

    priv->check_running = FALSE;
}

static gboolean
periodic_registration_check (MMIfaceModem3gpp *self)
{
    Private *priv;

    priv = get_private (self);

    /* Only launch a new one if not one running already */
    if (!priv->check_running) {
        priv->check_running = TRUE;
        mm_iface_modem_3gpp_run_registration_checks (
            self,
            (GAsyncReadyCallback)periodic_registration_checks_ready,
            NULL);
    }
    return G_SOURCE_CONTINUE;
}

static void
periodic_registration_check_disable (MMIfaceModem3gpp *self)
{
    Private *priv;

    priv = get_private (self);

    /* Do nothing if already disabled */
    if (!priv->check_timeout_source)
        return;

    g_source_remove (priv->check_timeout_source);
    priv->check_timeout_source = 0;

    mm_obj_dbg (self, "periodic 3GPP registration checks disabled");
}

static void
periodic_registration_check_enable (MMIfaceModem3gpp *self)
{
    Private *priv;

    priv = get_private (self);

    /* Do nothing if already enabled */
    if (priv->check_timeout_source)
        return;

    /* Create context and keep it as object data */
    mm_obj_dbg (self, "periodic 3GPP registration checks enabled");
    priv->check_timeout_source = g_timeout_add_seconds (REGISTRATION_CHECK_TIMEOUT_SEC,
                                                        (GSourceFunc)periodic_registration_check,
                                                        self);
}

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

void
mm_iface_modem_3gpp_update_pco_list (MMIfaceModem3gpp *self,
                                     const GList *pco_list)
{
    MmGdbusModem3gpp *skeleton = NULL;
    GVariantBuilder builder;
    GVariant *variant;
    const GList *iter;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &skeleton,
                  NULL);
    if (!skeleton)
        return;

    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ubay)"));
    for (iter = pco_list; iter; iter = g_list_next (iter)) {
        g_variant_builder_add_value (&builder,
                                     mm_pco_to_variant (MM_PCO (iter->data)));
    }
    variant = g_variant_ref_sink (g_variant_builder_end (&builder));
    mm_gdbus_modem3gpp_set_pco (skeleton, variant);
    g_variant_unref (variant);
    g_object_unref (skeleton);
}

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

void
mm_iface_modem_3gpp_update_initial_eps_bearer (MMIfaceModem3gpp   *self,
                                               MMBearerProperties *properties)
{
    MmGdbusModem3gpp *skeleton = NULL;
    MMBaseBearer     *old_bearer = NULL;

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON,      &skeleton,
                  MM_IFACE_MODEM_3GPP_INITIAL_EPS_BEARER, &old_bearer,
                  NULL);
    g_assert (skeleton);

    /* skip update? */
    if ((!old_bearer && !properties) ||
        (old_bearer && properties &&
         mm_bearer_properties_cmp (properties,
                                   mm_base_bearer_peek_config (MM_BASE_BEARER (old_bearer)),
                                   MM_BEARER_PROPERTIES_CMP_FLAGS_EPS)))
        goto out;

    if (properties) {
        MMBaseBearer *new_bearer;

        mm_obj_dbg (self, "updating initial EPS bearer...");
        g_assert (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->create_initial_eps_bearer);
        new_bearer = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->create_initial_eps_bearer (self, properties);
        g_object_set (self,
                      MM_IFACE_MODEM_3GPP_INITIAL_EPS_BEARER, new_bearer,
                      NULL);
        mm_gdbus_modem3gpp_set_initial_eps_bearer (skeleton, mm_base_bearer_get_path (new_bearer));
        g_object_unref (new_bearer);
    } else {
        mm_obj_dbg (self, "clearing initial EPS bearer...");
        g_object_set (self,
                      MM_IFACE_MODEM_3GPP_INITIAL_EPS_BEARER, NULL,
                      NULL);
        mm_gdbus_modem3gpp_set_initial_eps_bearer (skeleton, NULL);
    }

out:
    g_clear_object (&old_bearer);
    g_object_unref (skeleton);
}

static void
reload_initial_eps_bearer_ready (MMIfaceModem3gpp *self,
                                 GAsyncResult     *res)
{
    g_autoptr(MMBearerProperties) properties = NULL;
    g_autoptr(GError)             error = NULL;

    properties = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish (self, res, &error);
    if (!properties) {
        mm_obj_dbg (self, "couldn't load initial default bearer properties: %s", error->message);
        return;
    }

    mm_iface_modem_3gpp_update_initial_eps_bearer (self, properties);
}

void
mm_iface_modem_3gpp_reload_initial_eps_bearer (MMIfaceModem3gpp *self)
{
    if (get_eps_network_supported (self) &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish) {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer (
            self,
            (GAsyncReadyCallback)reload_initial_eps_bearer_ready,
            NULL);
    }
}

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

typedef struct _DisablingContext DisablingContext;
static void interface_disabling_step (GTask *task);

typedef enum {
    DISABLING_STEP_FIRST,
    DISABLING_STEP_INITIAL_EPS_BEARER,
    DISABLING_STEP_PERIODIC_REGISTRATION_CHECKS,
    DISABLING_STEP_DISABLE_UNSOLICITED_REGISTRATION_EVENTS,
    DISABLING_STEP_CLEANUP_UNSOLICITED_REGISTRATION_EVENTS,
    DISABLING_STEP_CLEANUP_UNSOLICITED_EVENTS,
    DISABLING_STEP_DISABLE_UNSOLICITED_EVENTS,
    DISABLING_STEP_REGISTRATION_STATE,
    DISABLING_STEP_LAST
} DisablingStep;

struct _DisablingContext {
    DisablingStep step;
    MmGdbusModem *skeleton;
};

static void
disabling_context_free (DisablingContext *ctx)
{
    if (ctx->skeleton)
        g_object_unref (ctx->skeleton);
    g_free (ctx);
}

gboolean
mm_iface_modem_3gpp_disable_finish (MMIfaceModem3gpp *self,
                                    GAsyncResult *res,
                                    GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

#undef VOID_REPLY_READY_FN
#define VOID_REPLY_READY_FN(NAME,DISPLAY)                               \
    static void                                                         \
    NAME##_ready (MMIfaceModem3gpp *self,                               \
                  GAsyncResult *res,                                    \
                  GTask *task)                                          \
    {                                                                   \
        DisablingContext *ctx;                                          \
        GError *error = NULL;                                           \
                                                                        \
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->NAME##_finish (self, res, &error); \
        if (error) {                                                    \
            mm_obj_dbg (self, "couldn't %s: %s", DISPLAY, error->message);      \
            g_error_free (error);                                       \
        }                                                               \
                                                                        \
        /* Go on to next step */                                        \
        ctx = g_task_get_task_data (task);                              \
        ctx->step++;                                                    \
        interface_disabling_step (task);                                \
    }

VOID_REPLY_READY_FN (cleanup_unsolicited_events,
                     "cleanup unsolicited events")
VOID_REPLY_READY_FN (disable_unsolicited_events,
                     "disable unsolicited events")
VOID_REPLY_READY_FN (cleanup_unsolicited_registration_events,
                     "cleanup unsolicited registration events")
VOID_REPLY_READY_FN (disable_unsolicited_registration_events,
                     "disable unsolicited registration events")

static void
interface_disabling_step (GTask *task)
{
    MMIfaceModem3gpp *self;
    DisablingContext *ctx;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    switch (ctx->step) {
    case DISABLING_STEP_FIRST:
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_INITIAL_EPS_BEARER:
        mm_iface_modem_3gpp_update_initial_eps_bearer (self, NULL);
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_PERIODIC_REGISTRATION_CHECKS:
        /* Disable periodic registration checks, if they were set */
        periodic_registration_check_disable (self);
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_DISABLE_UNSOLICITED_REGISTRATION_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_registration_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_registration_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_registration_events (
                self,
                get_cs_network_supported (self),
                get_ps_network_supported (self),
                get_eps_network_supported (self),
                (GAsyncReadyCallback)disable_unsolicited_registration_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_CLEANUP_UNSOLICITED_REGISTRATION_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_registration_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_registration_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_registration_events (
                self,
                (GAsyncReadyCallback)cleanup_unsolicited_registration_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_CLEANUP_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->cleanup_unsolicited_events (
                self,
                (GAsyncReadyCallback)cleanup_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_DISABLE_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->disable_unsolicited_events (
                self,
                (GAsyncReadyCallback)disable_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_REGISTRATION_STATE:
        update_registration_state (self, MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN);
        mm_iface_modem_3gpp_update_access_technologies (self, MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN);
        mm_iface_modem_3gpp_update_location (self, 0, 0, 0);
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_LAST:
        /* We are done without errors! */
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        g_assert_not_reached ();
    }

    g_assert_not_reached ();
}

void
mm_iface_modem_3gpp_disable (MMIfaceModem3gpp *self,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
{
    DisablingContext *ctx;
    GTask *task;

    ctx = g_new0 (DisablingContext, 1);
    ctx->step = DISABLING_STEP_FIRST;

    task = g_task_new (self, NULL, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)disabling_context_free);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &ctx->skeleton,
                  NULL);
    if (!ctx->skeleton) {
        g_task_return_new_error (task,
                                 MM_CORE_ERROR,
                                 MM_CORE_ERROR_FAILED,
                                 "Couldn't get interface skeleton");
        g_object_unref (task);
        return;
    }

    interface_disabling_step (task);
}

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

typedef struct _EnablingContext EnablingContext;
static void interface_enabling_step (GTask *task);

typedef enum {
    ENABLING_STEP_FIRST,
    ENABLING_STEP_SETUP_UNSOLICITED_EVENTS,
    ENABLING_STEP_ENABLE_UNSOLICITED_EVENTS,
    ENABLING_STEP_SETUP_UNSOLICITED_REGISTRATION_EVENTS,
    ENABLING_STEP_ENABLE_UNSOLICITED_REGISTRATION_EVENTS,
    ENABLING_STEP_INITIAL_EPS_BEARER,
    ENABLING_STEP_LAST
} EnablingStep;

struct _EnablingContext {
    EnablingStep step;
    MmGdbusModem3gpp *skeleton;
};

static void
enabling_context_free (EnablingContext *ctx)
{
    if (ctx->skeleton)
        g_object_unref (ctx->skeleton);
    g_free (ctx);
}

gboolean
mm_iface_modem_3gpp_enable_finish (MMIfaceModem3gpp *self,
                                   GAsyncResult *res,
                                   GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
setup_unsolicited_events_ready (MMIfaceModem3gpp *self,
                                GAsyncResult *res,
                                GTask *task)
{
    EnablingContext *ctx;
    GError *error = NULL;

    ctx = g_task_get_task_data (task);

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_events_finish (self, res, &error);
    if (error) {
        /* This error shouldn't be treated as critical */
        mm_obj_dbg (self, "setting up unsolicited events failed: %s", error->message);
        g_error_free (error);

        /* If we get an error setting up unsolicited events, don't even bother trying to
         * enable them. */
        ctx->step = ENABLING_STEP_ENABLE_UNSOLICITED_EVENTS + 1;
        interface_enabling_step (task);
        return;
    }

    /* Go on to next step */
    ctx->step++;
    interface_enabling_step (task);
}

static void
enable_unsolicited_events_ready (MMIfaceModem3gpp *self,
                                 GAsyncResult *res,
                                 GTask *task)
{
    EnablingContext *ctx;
    GError *error = NULL;

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_events_finish (self, res, &error);
    if (error) {
        /* This error shouldn't be treated as critical */
        mm_obj_dbg (self, "enabling unsolicited events failed: %s", error->message);
        g_error_free (error);
    }

    /* Go on to next step */
    ctx = g_task_get_task_data (task);
    ctx->step++;
    interface_enabling_step (task);
}

static void
setup_unsolicited_registration_events_ready (MMIfaceModem3gpp *self,
                                             GAsyncResult *res,
                                             GTask *task)
{
    EnablingContext *ctx;
    GError *error = NULL;

    ctx = g_task_get_task_data (task);

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_registration_events_finish (self, res, &error);
    if (error) {
        /* This error shouldn't be treated as critical */
        mm_obj_dbg (self, "setting up unsolicited registration events failed: %s", error->message);
        g_error_free (error);

        /* If we get an error setting up unsolicited events, don't even bother trying to
         * enable them. */
        ctx->step = ENABLING_STEP_ENABLE_UNSOLICITED_REGISTRATION_EVENTS + 1;
        interface_enabling_step (task);
        /* If error, setup periodic registration checks */
        periodic_registration_check_enable (self);
        return;
    }

    /* Go on to next step */
    ctx->step++;
    interface_enabling_step (task);
}

static void
enable_unsolicited_registration_events_ready (MMIfaceModem3gpp *self,
                                              GAsyncResult *res,
                                              GTask *task)
{
    EnablingContext *ctx;
    GError *error = NULL;

    MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_registration_events_finish (self, res, &error);
    if (error) {
        /* This error shouldn't be treated as critical */
        mm_obj_dbg (self, "enabling unsolicited registration events failed: %s", error->message);
        g_error_free (error);
        /* If error, setup periodic registration checks */
        periodic_registration_check_enable (self);
    }

    /* Go on to next step */
    ctx = g_task_get_task_data (task);
    ctx->step++;
    interface_enabling_step (task);
}

static void
load_initial_eps_bearer_ready (MMIfaceModem3gpp *self,
                               GAsyncResult     *res,
                               GTask            *task)
{
    MMBearerProperties *properties;
    EnablingContext    *ctx;
    GError             *error = NULL;

    ctx = g_task_get_task_data (task);

    properties = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish (self, res, &error);
    if (!properties) {
        mm_obj_dbg (self, "couldn't load initial default bearer properties: %s", error->message);
        g_error_free (error);
        goto out;
    }

    mm_iface_modem_3gpp_update_initial_eps_bearer (self, properties);
    g_object_unref (properties);

out:
    /* Go on to next step */
    ctx->step++;
    interface_enabling_step (task);
}

static void
interface_enabling_step (GTask *task)
{
    MMIfaceModem3gpp *self;
    EnablingContext *ctx;

    /* Don't run new steps if we're cancelled */
    if (g_task_return_error_if_cancelled (task)) {
        g_object_unref (task);
        return;
    }

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    switch (ctx->step) {
    case ENABLING_STEP_FIRST:
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_SETUP_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_events (
                self,
                (GAsyncReadyCallback)setup_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_ENABLE_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_events (
                self,
                (GAsyncReadyCallback)enable_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_SETUP_UNSOLICITED_REGISTRATION_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_registration_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_registration_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->setup_unsolicited_registration_events (
                self,
                (GAsyncReadyCallback)setup_unsolicited_registration_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_ENABLE_UNSOLICITED_REGISTRATION_EVENTS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_registration_events &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_registration_events_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->enable_unsolicited_registration_events (
                self,
                get_cs_network_supported (self),
                get_ps_network_supported (self),
                get_eps_network_supported (self),
                (GAsyncReadyCallback)enable_unsolicited_registration_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_INITIAL_EPS_BEARER:
        if (get_eps_network_supported (self) &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer (
                self,
                (GAsyncReadyCallback)load_initial_eps_bearer_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_LAST:
        /* We are done without errors! */
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        break;
    }

    g_assert_not_reached ();
}

void
mm_iface_modem_3gpp_enable (MMIfaceModem3gpp *self,
                            GCancellable *cancellable,
                            GAsyncReadyCallback callback,
                            gpointer user_data)
{
    EnablingContext *ctx;
    GTask *task;

    ctx = g_new0 (EnablingContext, 1);
    ctx->step = ENABLING_STEP_FIRST;

    task = g_task_new (self, cancellable, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)enabling_context_free);

    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &ctx->skeleton,
                  NULL);
    if (!ctx->skeleton) {
        g_task_return_new_error (task,
                                 MM_CORE_ERROR,
                                 MM_CORE_ERROR_FAILED,
                                 "Couldn't get interface skeleton");
        g_object_unref (task);
        return;
    }

    interface_enabling_step (task);
}

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

#if defined WITH_SUSPEND_RESUME

typedef struct _SyncingContext SyncingContext;
static void interface_syncing_step (GTask *task);

typedef enum {
    SYNCING_STEP_FIRST,
    SYNCING_STEP_REFRESH_3GPP_REGISTRATION,
    SYNCING_STEP_REFRESH_EPS_BEARER,
    SYNCING_STEP_LAST
} SyncingStep;

struct _SyncingContext {
    SyncingStep step;
};

gboolean
mm_iface_modem_3gpp_sync_finish (MMIfaceModem3gpp  *self,
                                 GAsyncResult      *res,
                                 GError           **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void
sync_eps_bearer_ready (MMIfaceModem3gpp *self,
                       GAsyncResult     *res,
                       GTask            *task)
{
    SyncingContext                *ctx;
    g_autoptr(MMBearerProperties)  properties  = NULL;
    g_autoptr(GError)              error = NULL;

    ctx = g_task_get_task_data (task);

    properties = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish (self, res, &error);
    if (!properties)
        mm_obj_dbg (self, "couldn't refresh initial EPS bearer status: %s", error->message);
    else
        mm_iface_modem_3gpp_update_initial_eps_bearer (self, properties);

    /* Go on to next step */
    ctx->step++;
    interface_syncing_step (task);
}

static void
sync_eps_bearer (MMIfaceModem3gpp    *self,
                 GAsyncReadyCallback  callback,
                 GTask               *task)
{
    SyncingContext *ctx;

    /* Refresh EPS bearer if supported */
    if (get_eps_network_supported (self) &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer &&
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_finish) {
        MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer (
            self,
            callback,
            task);
        return;
    }

    /* If EPS is unsupported, just go to the next step */
    ctx = g_task_get_task_data (task);
    ctx->step++;
    interface_syncing_step (task);
}

static void
sync_registration_ready (MMIfaceModem3gpp *self,
                         GAsyncResult     *res,
                         GTask            *task)
{
    SyncingContext     *ctx;
    g_autoptr (GError)  error = NULL;

    ctx = g_task_get_task_data (task);

    if (!mm_iface_modem_3gpp_run_registration_checks_finish (self, res, &error))
        mm_obj_dbg (self, "couldn't synchronize 3GPP registration: %s", error->message);

    /* Go on to next step */
    ctx->step++;
    interface_syncing_step(task);
}

static void
interface_syncing_step (GTask *task)
{
    MMIfaceModem3gpp *self;
    SyncingContext   *ctx;

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    switch (ctx->step) {
    case SYNCING_STEP_FIRST:
        ctx->step++;
        /* fall through */

    case SYNCING_STEP_REFRESH_3GPP_REGISTRATION:
        /*
         * Refresh registration info to verify that the modem is still registered.
         * Wait until registration checks are complete before going to the next step.
         */
        mm_iface_modem_3gpp_run_registration_checks (
            self,
            (GAsyncReadyCallback)sync_registration_ready,
            task);
        return;

    case SYNCING_STEP_REFRESH_EPS_BEARER:
        /*
         * Refresh EPS bearer and wait until complete.
         * We want to make sure that the modem is fully enabled again
         * when we refresh the mobile data connection bearers.
         */
        sync_eps_bearer (
            self,
            (GAsyncReadyCallback)sync_eps_bearer_ready,
            task);
        return;

    case SYNCING_STEP_LAST:
        /* We are done without errors! */
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        break;
    }

    g_assert_not_reached ();
}

void
mm_iface_modem_3gpp_sync (MMIfaceModem3gpp    *self,
                          GAsyncReadyCallback  callback,
                          gpointer             user_data)
{
    SyncingContext *ctx;
    GTask          *task;

    /* Create SyncingContext */
    ctx = g_new0 (SyncingContext, 1);
    ctx->step = SYNCING_STEP_FIRST;

    /* Create sync steps task and execute it */
    task = g_task_new (self, NULL, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)g_free);
    interface_syncing_step (task);
}

#endif

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

typedef struct _InitializationContext InitializationContext;
static void interface_initialization_step (GTask *task);

typedef enum {
    INITIALIZATION_STEP_FIRST,
    INITIALIZATION_STEP_ENABLED_FACILITY_LOCKS,
    INITIALIZATION_STEP_TEST_LOCKED,
    INITIALIZATION_STEP_IMEI,
    INITIALIZATION_STEP_EPS_UE_MODE_OPERATION,
    INITIALIZATION_STEP_EPS_INITIAL_BEARER_SETTINGS,
    INITIALIZATION_STEP_NR5G_REGISTRATION_SETTINGS,
    INITIALIZATION_STEP_CONNECT_SIGNALS,
    INITIALIZATION_STEP_LAST
} InitializationStep;

struct _InitializationContext {
    MmGdbusModem3gpp *skeleton;
    InitializationStep step;
};

static void
initialization_context_free (InitializationContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_free (ctx);
}

static void
sim_pin_lock_enabled_cb (MMBaseSim *self,
                         gboolean enabled,
                         MmGdbusModem3gpp *skeleton)
{
    MMModem3gppFacility facilities;

    facilities = mm_gdbus_modem3gpp_get_enabled_facility_locks (skeleton);
    if (enabled)
        facilities |= MM_MODEM_3GPP_FACILITY_SIM;
    else
        facilities &= ~MM_MODEM_3GPP_FACILITY_SIM;

    mm_gdbus_modem3gpp_set_enabled_facility_locks (skeleton, facilities);
}

static void
load_nr5g_registration_settings_ready (MMIfaceModem3gpp *self,
                                       GAsyncResult     *res,
                                       GTask            *task)
{
    InitializationContext                 *ctx;
    g_autoptr(GError)                      error = NULL;
    g_autoptr(MMNr5gRegistrationSettings)  settings = NULL;

    ctx = g_task_get_task_data (task);

    settings = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings_finish (self, res, &error);
    if (!settings) {
        mm_obj_warn (self, "couldn't load 5GNR registration settings: %s", error->message);
    } else {
        g_autoptr(GVariant) dictionary = NULL;

        dictionary = mm_nr5g_registration_settings_get_dictionary (settings);
        mm_gdbus_modem3gpp_set_nr5g_registration_settings (ctx->skeleton, dictionary);
    }

    /* Go on to next step */
    ctx->step++;
    interface_initialization_step (task);
}

static void
load_initial_eps_bearer_settings_ready (MMIfaceModem3gpp *self,
                                        GAsyncResult     *res,
                                        GTask            *task)
{
    InitializationContext *ctx;
    MMBearerProperties    *config;
    GError                *error = NULL;

    ctx = g_task_get_task_data (task);

    config = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings_finish (self, res, &error);
    if (!config) {
        mm_obj_warn (self, "couldn't load initial EPS bearer settings: %s", error->message);
        g_error_free (error);
    } else {
        GVariant *dictionary;

        dictionary = mm_bearer_properties_get_dictionary (config);
        mm_gdbus_modem3gpp_set_initial_eps_bearer_settings (ctx->skeleton, dictionary);
        g_object_unref (config);
        if (dictionary)
            g_variant_unref (dictionary);
    }

    /* Go on to next step */
    ctx->step++;
    interface_initialization_step (task);
}

static void
load_eps_ue_mode_operation_ready (MMIfaceModem3gpp *self,
                                  GAsyncResult     *res,
                                  GTask            *task)
{
    InitializationContext         *ctx;
    MMModem3gppEpsUeModeOperation  uemode;
    GError                        *error = NULL;

    ctx = g_task_get_task_data (task);

    uemode = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation_finish (self, res, &error);
    mm_gdbus_modem3gpp_set_eps_ue_mode_operation (ctx->skeleton, uemode);

    if (error) {
        mm_obj_warn (self, "couldn't load UE mode of operation for EPS: %s", error->message);
        g_error_free (error);
    }

    /* Go on to next step */
    ctx->step++;
    interface_initialization_step (task);
}

static void
load_enabled_facility_locks_ready (MMIfaceModem3gpp *self,
                                   GAsyncResult *res,
                                   GTask *task)
{
    InitializationContext *ctx;
    GError *error = NULL;
    MMModem3gppFacility facilities;

    ctx = g_task_get_task_data (task);

    facilities = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_enabled_facility_locks_finish (self, res, &error);
    mm_gdbus_modem3gpp_set_enabled_facility_locks (ctx->skeleton, facilities);

    if (error) {
        mm_obj_warn (self, "couldn't load facility locks: %s", error->message);
        g_error_free (error);
    } else {
        MMBaseSim *sim = NULL;

        /* We loaded the initial list of facility locks; but we do need to update
         * the SIM PIN lock status when that changes. We'll connect to the signal
         * which notifies about such update. There is no need to ref self as the
         * SIM itself is an object which exists as long as self exists. */
        g_object_get (self,
                      MM_IFACE_MODEM_SIM, &sim,
                      NULL);
        g_signal_connect (sim,
                          MM_BASE_SIM_PIN_LOCK_ENABLED,
                          G_CALLBACK (sim_pin_lock_enabled_cb),
                          ctx->skeleton);
        g_object_unref (sim);
    }

    /* Go on to next step */
    ctx->step++;
    interface_initialization_step (task);
}

static void
load_imei_ready (MMIfaceModem3gpp *self,
                 GAsyncResult *res,
                 GTask *task)
{
    InitializationContext *ctx;
    GError *error = NULL;
    gchar *imei;

    ctx = g_task_get_task_data (task);

    imei = MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_imei_finish (self, res, &error);
    mm_gdbus_modem3gpp_set_imei (ctx->skeleton, imei);
    g_free (imei);

    if (error) {
        mm_obj_warn (self, "couldn't load IMEI: %s", error->message);
        g_error_free (error);
    }

    /* Go on to next step */
    ctx->step++;
    interface_initialization_step (task);
}

static void
interface_initialization_step (GTask *task)
{
    MMIfaceModem3gpp *self;
    InitializationContext *ctx;
    MMModemState modem_state;

    /* Don't run new steps if we're cancelled */
    if (g_task_return_error_if_cancelled (task)) {
        g_object_unref (task);
        return;
    }

    self = g_task_get_source_object (task);
    ctx = g_task_get_task_data (task);

    switch (ctx->step) {
    case INITIALIZATION_STEP_FIRST:
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_ENABLED_FACILITY_LOCKS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_enabled_facility_locks &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_enabled_facility_locks_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_enabled_facility_locks (
                self,
                (GAsyncReadyCallback)load_enabled_facility_locks_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_TEST_LOCKED:
        modem_state = MM_MODEM_STATE_UNKNOWN;
        g_object_get (self,
                      MM_IFACE_MODEM_STATE, &modem_state,
                      NULL);
        if (modem_state == MM_MODEM_STATE_LOCKED) {
            /* Skip some steps and export the interface if modem is locked */
            ctx->step = INITIALIZATION_STEP_LAST;
            interface_initialization_step (task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_IMEI:
        /* IMEI value is meant to be loaded only once during the whole
         * lifetime of the modem. Therefore, if we already have it loaded,
         * don't try to load it again. */
        if (!mm_gdbus_modem3gpp_get_imei (ctx->skeleton) &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_imei &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_imei_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_imei (
                self,
                (GAsyncReadyCallback)load_imei_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_EPS_UE_MODE_OPERATION:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_eps_ue_mode_operation (
                self,
                (GAsyncReadyCallback)load_eps_ue_mode_operation_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_EPS_INITIAL_BEARER_SETTINGS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_initial_eps_bearer_settings (
                self,
                (GAsyncReadyCallback)load_initial_eps_bearer_settings_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_NR5G_REGISTRATION_SETTINGS:
        if (MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings &&
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings_finish) {
            MM_IFACE_MODEM_3GPP_GET_INTERFACE (self)->load_nr5g_registration_settings (
                self,
                (GAsyncReadyCallback)load_nr5g_registration_settings_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_CONNECT_SIGNALS:
        /* We are done without errors! */

        /* Handle method invocations */
        g_signal_connect (ctx->skeleton,
                          "handle-register",
                          G_CALLBACK (handle_register),
                          self);
        g_signal_connect (ctx->skeleton,
                          "handle-scan",
                          G_CALLBACK (handle_scan),
                          self);
        g_signal_connect (ctx->skeleton,
                          "handle-set-eps-ue-mode-operation",
                          G_CALLBACK (handle_set_eps_ue_mode_operation),
                          self);
        g_signal_connect (ctx->skeleton,
                          "handle-set-initial-eps-bearer-settings",
                          G_CALLBACK (handle_set_initial_eps_bearer_settings),
                          self);
        g_signal_connect (ctx->skeleton,
                          "handle-set-packet-service-state",
                          G_CALLBACK (handle_set_packet_service_state),
                          self);
        g_signal_connect (ctx->skeleton,
                          "handle-set-nr5g-registration-settings",
                          G_CALLBACK (handle_set_nr5g_registration_settings),
                          self);

        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_LAST:
        /* Always connect the signal to unlock modem */
        g_signal_connect (ctx->skeleton,
                          "handle-disable-facility-lock",
                          G_CALLBACK (handle_disable_facility_lock),
                          self);

        /* Finally, export the new interface */
        mm_gdbus_object_skeleton_set_modem3gpp (MM_GDBUS_OBJECT_SKELETON (self),
                                                MM_GDBUS_MODEM3GPP (ctx->skeleton));

        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        break;
    }

    g_assert_not_reached ();
}

gboolean
mm_iface_modem_3gpp_initialize_finish (MMIfaceModem3gpp *self,
                                       GAsyncResult *res,
                                       GError **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

void
mm_iface_modem_3gpp_initialize (MMIfaceModem3gpp *self,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer user_data)
{
    MmGdbusModem3gpp *skeleton = NULL;
    InitializationContext *ctx;
    GTask *task;

    /* Did we already create it? */
    g_object_get (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, &skeleton,
                  NULL);
    if (!skeleton) {
        skeleton = mm_gdbus_modem3gpp_skeleton_new ();

        /* Set all initial property defaults */
        mm_gdbus_modem3gpp_set_imei (skeleton, NULL);
        mm_gdbus_modem3gpp_set_operator_code (skeleton, NULL);
        mm_gdbus_modem3gpp_set_operator_name (skeleton, NULL);
        mm_gdbus_modem3gpp_set_enabled_facility_locks (skeleton, MM_MODEM_3GPP_FACILITY_NONE);
        mm_gdbus_modem3gpp_set_subscription_state (skeleton, MM_MODEM_3GPP_SUBSCRIPTION_STATE_UNKNOWN);
        mm_gdbus_modem3gpp_set_pco (skeleton, NULL);
        mm_gdbus_modem3gpp_set_initial_eps_bearer (skeleton, NULL);

        /* Bind our RegistrationState property */
        g_object_bind_property (self, MM_IFACE_MODEM_3GPP_REGISTRATION_STATE,
                                skeleton, "registration-state",
                                G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

        /* Bind our packet service state property */
        g_object_bind_property (self, MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE,
                                skeleton, "packet-service-state",
                                G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

        g_object_set (self,
                      MM_IFACE_MODEM_3GPP_DBUS_SKELETON, skeleton,
                      NULL);
    }

    ctx = g_new0 (InitializationContext, 1);
    ctx->step = INITIALIZATION_STEP_FIRST;
    ctx->skeleton = skeleton;

    task = g_task_new (self, cancellable, callback, user_data);
    g_task_set_task_data (task, ctx, (GDestroyNotify)initialization_context_free);

    /* Perform async initialization here */
    interface_initialization_step (task);
}

void
mm_iface_modem_3gpp_shutdown (MMIfaceModem3gpp *self)
{
    /* Unexport DBus interface and remove the skeleton */
    mm_gdbus_object_skeleton_set_modem3gpp (MM_GDBUS_OBJECT_SKELETON (self), NULL);
    g_object_set (self,
                  MM_IFACE_MODEM_3GPP_DBUS_SKELETON, NULL,
                  NULL);
}

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

static void
iface_modem_3gpp_init (gpointer g_iface)
{
    static gboolean initialized = FALSE;

    if (initialized)
        return;

    /* Properties */
    g_object_interface_install_property
        (g_iface,
         g_param_spec_object (MM_IFACE_MODEM_3GPP_DBUS_SKELETON,
                              "3GPP DBus skeleton",
                              "DBus skeleton for the 3GPP interface",
                              MM_GDBUS_TYPE_MODEM3GPP_SKELETON,
                              G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_enum (MM_IFACE_MODEM_3GPP_REGISTRATION_STATE,
                            "RegistrationState",
                            "Registration state of the modem",
                            MM_TYPE_MODEM_3GPP_REGISTRATION_STATE,
                            MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN,
                            G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_3GPP_CS_NETWORK_SUPPORTED,
                               "CS network supported",
                               "Whether the modem works in the CS network",
                               TRUE,
                               G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_3GPP_PS_NETWORK_SUPPORTED,
                               "PS network supported",
                               "Whether the modem works in the PS network",
                               TRUE,
                               G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_3GPP_EPS_NETWORK_SUPPORTED,
                               "EPS network supported",
                               "Whether the modem works in the EPS network",
                               FALSE,
                               G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_3GPP_5GS_NETWORK_SUPPORTED,
                               "5GS network supported",
                               "Whether the modem works in the 5GS network",
                               FALSE,
                               G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_flags (MM_IFACE_MODEM_3GPP_IGNORED_FACILITY_LOCKS,
                             "Ignored locks",
                             "Ignored facility locks",
                             MM_TYPE_MODEM_3GPP_FACILITY,
                             MM_MODEM_3GPP_FACILITY_NONE,
                             G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_object (MM_IFACE_MODEM_3GPP_INITIAL_EPS_BEARER,
                              "Initial EPS bearer",
                              "Initial EPS bearer setup during registration",
                              MM_TYPE_BASE_BEARER,
                              G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_enum   (MM_IFACE_MODEM_3GPP_PACKET_SERVICE_STATE,
                              "PacketServiceState",
                              "Packet service state of the modem",
                              MM_TYPE_MODEM_3GPP_PACKET_SERVICE_STATE,
                              MM_MODEM_3GPP_PACKET_SERVICE_STATE_UNKNOWN,
                              G_PARAM_READWRITE));

    initialized = TRUE;
}

GType
mm_iface_modem_3gpp_get_type (void)
{
    static GType iface_modem_3gpp_type = 0;

    if (!G_UNLIKELY (iface_modem_3gpp_type)) {
        static const GTypeInfo info = {
            sizeof (MMIfaceModem3gpp), /* class_size */
            iface_modem_3gpp_init,      /* base_init */
            NULL,                  /* base_finalize */
        };

        iface_modem_3gpp_type = g_type_register_static (G_TYPE_INTERFACE,
                                                        "MMIfaceModem3gpp",
                                                        &info,
                                                        0);

        g_type_interface_add_prerequisite (iface_modem_3gpp_type, MM_TYPE_IFACE_MODEM);
    }

    return iface_modem_3gpp_type;
}
