/* -*- 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) 2015 Marco Bascetta <marco.bascetta@sadel.it>
 * Copyright (C) 2019 Purism SPC
 */

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

#include "mm-iface-modem.h"
#include "mm-iface-modem-voice.h"
#include "mm-call-list.h"
#include "mm-log-object.h"

#define CALL_LIST_POLLING_CONTEXT_TAG "voice-call-list-polling-context-tag"
#define IN_CALL_EVENT_CONTEXT_TAG     "voice-in-call-event-context-tag"

static GQuark call_list_polling_context_quark;
static GQuark in_call_event_context_quark;

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

void
mm_iface_modem_voice_bind_simple_status (MMIfaceModemVoice *self,
                                         MMSimpleStatus *status)
{
}

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

gboolean
mm_iface_modem_voice_authorize_outgoing_call (MMIfaceModemVoice  *self,
                                              MMBaseCall         *call,
                                              GError            **error)
{
    MmGdbusModemVoice *skeleton = NULL;
    MMBaseSim         *sim = NULL;
    gboolean           emergency_only = FALSE;
    gboolean           call_allowed = FALSE;
    GError            *inner_error = NULL;
    guint              i;
    const gchar       *number;

    static const gchar *always_valid_emergency_numbers[] = { "112", "911" };
    static const gchar *no_sim_valid_emergency_numbers[] = { "000", "08", "110", "999", "118", "119" };

    g_assert (mm_base_call_get_direction (call) == MM_CALL_DIRECTION_OUTGOING);
    number = mm_base_call_get_number (call);
    g_assert (number);

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_DBUS_SKELETON, &skeleton,
                  NULL);

    if (!skeleton) {
        g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                     "voice operations unsupported");
        goto out;
    }

    g_object_get (skeleton,
                  "emergency-only", &emergency_only,
                  NULL);

    /* Identification of emergency numbers. 3GPP TS 22.101
     *
     *   a) 112 and 911 shall always be available.
     *   b) Any emergency call number stored on a SIM/USIM when the SIM/USIM is
     *      present.
     *   c) 000, 08, 110, 999, 118 and 119 when a SIM/USIM is not present.
     *   d) Additional emergency call numbers that may have been downloaded by
     *      the serving network when the SIM/USIM is present.
     *
     * In ModemManager we're not flagging any call as being "emergency" or
     * "normal", but we can right away limit non-emergency calls if we're in
     * "emergency-only" mode.
     */

    /* If we're not in emergency mode, the call (emergency or normal) is always allowed */
    if (!emergency_only) {
        mm_obj_dbg (self, "voice call to %s allowed", number);
        call_allowed = TRUE;
        goto out;
    }

    for (i = 0; i < G_N_ELEMENTS (always_valid_emergency_numbers); i++) {
        if (g_strcmp0 (number, always_valid_emergency_numbers[i]) == 0) {
            mm_obj_dbg (self, "voice call to %s allowed: emergency call number always valid", number);
            call_allowed = TRUE;
            goto out;
        }
    }

    /* Check if we have a SIM */
    g_object_get (self,
                  MM_IFACE_MODEM_SIM, &sim,
                  NULL);
    if (!sim) {
        /* If no SIM available, some additional numbers may be valid emergency numbers */
        for (i = 0; i < G_N_ELEMENTS (no_sim_valid_emergency_numbers); i++) {
            if (g_strcmp0 (number, no_sim_valid_emergency_numbers[i]) == 0) {
                mm_obj_dbg (self, "voice call to %s allowed: emergency call number valid when no SIM", number);
                call_allowed = TRUE;
                goto out;
            }
        }

        mm_obj_dbg (self, "voice call to %s NOT allowed: not a valid emergency call number when no SIM", number);
        goto out;
    }

    /* Check if the number is programmed in EF_ECC */
    if (mm_base_sim_is_emergency_number (sim, number)) {
        mm_obj_dbg (self, "voice call to %s allowed: emergency call number programmed in the SIM", number);
        call_allowed = TRUE;
    } else
        mm_obj_dbg (self, "voice call to %s NOT allowed: not a valid emergency call number programmed in the SIM", number);

 out:

    if (inner_error)
        g_propagate_error (error, inner_error);
    else if (!call_allowed)
        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED,
                     "only emergency calls allowed");

    g_clear_object (&skeleton);
    g_clear_object (&sim);
    return call_allowed;
}

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

/* new calls will inherit audio settings if the modem is already in-call state */
static void update_audio_settings_in_call (MMIfaceModemVoice *self,
                                           MMBaseCall        *call);

static MMBaseCall *
create_incoming_call (MMIfaceModemVoice *self,
                      const gchar       *number)
{
    MMBaseCall *call;

    g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL);

    call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_INCOMING, number);
    update_audio_settings_in_call (self, call);
    return call;
}

static MMBaseCall *
create_outgoing_call_from_properties (MMIfaceModemVoice  *self,
                                      MMCallProperties   *properties,
                                      GError            **error)
{
    MMBaseCall  *call;
    const gchar *number;

    /* Don't create CALL from properties if either number is missing */
    number = mm_call_properties_get_number (properties) ;
    if (!number) {
        g_set_error (error,
                     MM_CORE_ERROR,
                     MM_CORE_ERROR_INVALID_ARGS,
                     "Cannot create call: mandatory parameter 'number' is missing");
        return NULL;
    }

    /* Create a call object as defined by the interface */
    g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call != NULL);
    call = MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->create_call (self, MM_CALL_DIRECTION_OUTGOING, number);
    update_audio_settings_in_call (self, call);
    return call;
}

/*****************************************************************************/
/* Common helper to match call info against a known call object */

static gboolean
match_single_call_info (MMIfaceModemVoice *self,
                        const MMCallInfo  *call_info,
                        MMBaseCall        *call)
{
    MMCallState      state;
    MMCallDirection  direction;
    const gchar     *number;
    guint            idx;
    gboolean         match_direction_and_state = FALSE;
    gboolean         match_number = FALSE;
    gboolean         match_index = FALSE;
    gboolean         match_terminated = FALSE;

    /* try to look for a matching call by direction/number/index */
    state     = mm_base_call_get_state     (call);
    direction = mm_base_call_get_direction (call);
    number    = mm_base_call_get_number    (call);
    idx       = mm_base_call_get_index     (call);

    /* Match index */
    if (call_info->index && (call_info->index == idx))
        match_index = TRUE;

    /* Match direction and state.
     * We cannot apply this match if both call info and call have an index set
     * and they're different already. */
    if ((call_info->direction == direction) &&
        (call_info->state == state) &&
        (!call_info->index || !idx || match_index))
        match_direction_and_state = TRUE;

    /* Match number */
    if (call_info->number && number &&
        g_strcmp0 (call_info->number, number) == 0)
        match_number = TRUE;

    /* Match special terminated event.
     * We cannot apply this match if the call is part of a multiparty
     * call, because we don't know which of the calls in the multiparty
     * is the one that finished. Must rely on other reports that do
     * provide call index. */
    if ((call_info->state == MM_CALL_STATE_TERMINATED) &&
        (call_info->direction == MM_CALL_DIRECTION_UNKNOWN) &&
        !call_info->index &&
        !call_info->number &&
        !mm_base_call_get_multiparty (call))
        match_terminated = TRUE;

    /* If no clear match, nothing to do */
    if (!match_direction_and_state &&
        !match_number &&
        !match_index &&
        !match_terminated)
        return FALSE;

    mm_obj_dbg (self, "call info matched (matched direction/state %s, matched number %s"
                ", matched index %s, matched terminated %s) with call at '%s'",
                match_direction_and_state ? "yes" : "no",
                match_number ? "yes" : "no",
                match_index ? "yes" : "no",
                match_terminated ? "yes" : "no",
                mm_base_call_get_path (call));

    /* Early detect if a known incoming call that was created
     * from a plain CRING URC (i.e. without caller number)
     * needs to have the number provided.
     */
    if (call_info->number && !number) {
        mm_obj_dbg (self, "  number set: %s", call_info->number);
        mm_base_call_set_number (call, call_info->number);
    }

    /* Early detect if a known incoming/outgoing call does
     * not have a known call index yet.
     */
    if (call_info->index && !idx) {
        mm_obj_dbg (self, "  index set: %u", call_info->index);
        mm_base_call_set_index (call, call_info->index);
    }

    /* Update state if it changed */
    if (call_info->state != state) {
        mm_obj_dbg (self, "  state updated: %s", mm_call_state_get_string (call_info->state));
        mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_UNKNOWN);
    }

    /* refresh if incoming and new state is not terminated */
    if ((call_info->state != MM_CALL_STATE_TERMINATED) &&
        (direction == MM_CALL_DIRECTION_INCOMING)) {
        mm_obj_dbg (self, "  incoming refreshed");
        mm_base_call_incoming_refresh (call);
    }

    return TRUE;
}

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

typedef struct {
    MMIfaceModemVoice *self;
    const MMCallInfo  *call_info;
} ReportCallForeachContext;

static void
report_call_foreach (MMBaseCall               *call,
                     ReportCallForeachContext *ctx)
{
    /* Do nothing if already matched */
    if (!ctx->call_info)
        return;

    /* fully ignore already terminated calls */
    if (mm_base_call_get_state (call) == MM_CALL_STATE_TERMINATED)
        return;

    /* Reset call info in context if the call info matches an existing call */
    if (match_single_call_info (ctx->self, ctx->call_info, call))
        ctx->call_info = NULL;
}

void
mm_iface_modem_voice_report_call (MMIfaceModemVoice *self,
                                  const MMCallInfo  *call_info)
{
    ReportCallForeachContext  ctx = { 0 };
    MMBaseCall               *call = NULL;
    MMCallList               *list = NULL;

    /* When reporting single call, the only mandatory parameter is the state:
     *   - index is optional (e.g. unavailable when receiving +CLIP URCs)
     *   - number is optional (e.g. unavailable when receiving +CRING URCs)
     *   - direction is optional (e.g. unavailable when receiving some vendor-specific URCs)
     */
    g_assert (call_info->state != MM_CALL_STATE_UNKNOWN);

    /* Early debugging of the call state update */
    mm_obj_dbg (self, "call at index %u: direction %s, state %s, number %s",
                call_info->index,
                mm_call_direction_get_string (call_info->direction),
                mm_call_state_get_string (call_info->state),
                call_info->number ? call_info->number : "n/a");

    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);

    if (!list) {
        mm_obj_warn (self, "cannot process call state update: missing call list");
        return;
    }

    /* Iterate over all known calls and try to match a known one */
    ctx.self = self;
    ctx.call_info = call_info;
    mm_call_list_foreach (list, (MMCallListForeachFunc)report_call_foreach, &ctx);

    /* If call info matched with an existing one, the context call info would have been reseted */
    if (!ctx.call_info)
        goto out;

    /* If call info didn't match with any known call, it may be because we're being
     * reported a NEW incoming call. If that's not the case, we'll ignore the report. */
    if ((call_info->direction != MM_CALL_DIRECTION_INCOMING) ||
        ((call_info->state != MM_CALL_STATE_WAITING) && (call_info->state != MM_CALL_STATE_RINGING_IN))) {
        mm_obj_dbg (self, "unhandled call state update reported: direction: %s, state %s",
                    mm_call_direction_get_string (call_info->direction),
                    mm_call_state_get_string (call_info->state));
        goto out;
    }

    mm_obj_dbg (self, "creating new incoming call...");
    call = create_incoming_call (self, call_info->number);

    /* Set the state */
    mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_INCOMING_NEW);

    /* Set the index, if known */
    if (call_info->index)
        mm_base_call_set_index (call, call_info->index);

    /* Start its validity timeout */
    mm_base_call_incoming_refresh (call);

    /* Only export once properly created */
    mm_base_call_export (call);
    mm_call_list_add_call (list, call);
    g_object_unref (call);

 out:
    g_object_unref (list);
}

/*****************************************************************************/
/* Full current call list reporting
 *
 * This method receives as input a list with all the currently active calls,
 * including the specific state they're in.
 *
 * This method should:
 *  - Check whether we're reporting a new call (i.e. not in our internal call
 *    list yet). We'll create a new call object if so.
 *  - Check whether any of the known calls has changed state, and if so,
 *    update it.
 *  - Check whether any of the known calls is NOT given in the input list of
 *    call infos, which would mean the call is terminated.
 */

typedef struct {
    MMIfaceModemVoice *self;
    GList             *call_info_list;
} ReportAllCallsForeachContext;

static void
report_all_calls_foreach (MMBaseCall                   *call,
                          ReportAllCallsForeachContext *ctx)
{
    MMCallState state;
    GList *l;

    /* fully ignore already terminated calls */
    state = mm_base_call_get_state (call);
    if (state == MM_CALL_STATE_TERMINATED)
        return;

    /* Iterate over the call info list */
    for (l = ctx->call_info_list; l; l = g_list_next (l)) {
        MMCallInfo *call_info = (MMCallInfo *)(l->data);

        /* if match found, delete item from list and halt iteration right away */
        if (match_single_call_info (ctx->self, call_info, call)) {
            ctx->call_info_list = g_list_delete_link (ctx->call_info_list, l);
            return;
        }
    }

    /* not found in list! this call is now terminated */
    mm_obj_dbg (ctx->self, "call '%s' with direction %s, state %s, number '%s', index %u"
                " not found in list, terminating",
                mm_base_call_get_path (call),
                mm_call_direction_get_string (mm_base_call_get_direction (call)),
                mm_call_state_get_string (state),
                mm_base_call_get_number (call),
                mm_base_call_get_index (call));
    mm_base_call_change_state (call, MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_UNKNOWN);
}

void
mm_iface_modem_voice_report_all_calls (MMIfaceModemVoice *self,
                                       GList             *call_info_list)
{
    ReportAllCallsForeachContext  ctx = { 0 };
    MMCallList                   *list = NULL;
    GList                        *l;

    /* Early debugging of the full list of calls */
    mm_obj_dbg (self, "reported %u ongoing calls", g_list_length (call_info_list));
    for (l = call_info_list; l; l = g_list_next (l)) {
        MMCallInfo *call_info = (MMCallInfo *)(l->data);

        /* When reporting full list of calls, index and state are mandatory */
        g_assert (call_info->index != 0);
        g_assert (call_info->state != MM_CALL_STATE_UNKNOWN);

        mm_obj_dbg (self, "call at index %u: direction %s, state %s, number %s",
                    call_info->index,
                    mm_call_direction_get_string (call_info->direction),
                    mm_call_state_get_string (call_info->state),
                    call_info->number ? call_info->number : "n/a");
    }

    /* Retrieve list of known calls */
    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        mm_obj_warn (self, "cannot report all calls: missing call list");
        return;
    }

    /* Iterate over all the calls already known to us.
     * Whenever a known call is updated, it will be removed from the call info list */
    ctx.self = self;
    ctx.call_info_list = g_list_copy (call_info_list);
    mm_call_list_foreach (list, (MMCallListForeachFunc)report_all_calls_foreach, &ctx);

    /* Once processed, the call info list will have all calls that were unknown to
     * us, i.e. the new calls to create. We really only expect new incoming calls, so
     * we'll warn if we get any outgoing call reported here. */
    for (l = ctx.call_info_list; l; l = g_list_next (l)) {
        MMCallInfo *call_info = (MMCallInfo *)(l->data);

        /* Ignore unknown terminated calls, because these be due to an already
         * processed event. */
        if (call_info->state == MM_CALL_STATE_TERMINATED)
            continue;

        if (call_info->direction == MM_CALL_DIRECTION_OUTGOING) {
            mm_obj_warn (self, "unexpected outgoing call to number '%s' reported in call list: state %s",
                         call_info->number ? call_info->number : "n/a",
                         mm_call_state_get_string (call_info->state));
            continue;
        }

        if (call_info->direction == MM_CALL_DIRECTION_INCOMING) {
            MMBaseCall *call;

            /* We only expect either RINGING-IN or WAITING states */
            if ((call_info->state != MM_CALL_STATE_RINGING_IN) &&
                (call_info->state != MM_CALL_STATE_WAITING)) {
                    mm_obj_warn (self, "unexpected incoming call to number '%s' reported in call list: state %s",
                                 call_info->number ? call_info->number : "n/a",
                                 mm_call_state_get_string (call_info->state));
                    continue;
            }

            mm_obj_dbg (self, "creating new incoming call...");
            call = create_incoming_call (self, call_info->number);

            /* Set the state and the index */
            mm_base_call_change_state (call, call_info->state, MM_CALL_STATE_REASON_INCOMING_NEW);
            mm_base_call_set_index (call, call_info->index);

            /* Start its validity timeout */
            mm_base_call_incoming_refresh (call);

            /* Only export once properly created */
            mm_base_call_export (call);
            mm_call_list_add_call (list, call);
            g_object_unref (call);
            continue;
        }

        mm_obj_warn (self, "unexpected call to number '%s' reported in call list: state %s, direction unknown",
                     call_info->number ? call_info->number : "n/a",
                     mm_call_state_get_string (call_info->state));
    }
    g_list_free (ctx.call_info_list);
    g_object_unref (list);
}

/*****************************************************************************/
/* Incoming DTMF reception, not associated to a specific call */

typedef struct {
    guint        index;
    const gchar *dtmf;
} ReceivedDtmfContext;

static void
received_dtmf_foreach (MMBaseCall          *call,
                       ReceivedDtmfContext *ctx)
{
    if ((!ctx->index || (ctx->index == mm_base_call_get_index (call))) &&
        (mm_base_call_get_state (call) == MM_CALL_STATE_ACTIVE))
        mm_base_call_received_dtmf (call, ctx->dtmf);
}

void
mm_iface_modem_voice_received_dtmf (MMIfaceModemVoice *self,
                                    guint              index,
                                    const gchar       *dtmf)
{
    MMCallList          *list = NULL;
    ReceivedDtmfContext  ctx = {
        .index = index,
        .dtmf  = dtmf
    };

    /* Retrieve list of known calls */
    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        mm_obj_warn (self, "cannot report received DTMF: missing call list");
        return;
    }

    mm_call_list_foreach (list, (MMCallListForeachFunc)received_dtmf_foreach, &ctx);
    g_object_unref (list);
}

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

typedef struct {
    MmGdbusModemVoice *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice *self;
    gchar *path;
} HandleDeleteContext;

static void
handle_delete_context_free (HandleDeleteContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_free (ctx->path);
    g_free (ctx);
}

static void
handle_delete_auth_ready (MMBaseModem *self,
                          GAsyncResult *res,
                          HandleDeleteContext *ctx)
{
    MMCallList *list = NULL;
    GError *error = NULL;

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

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot delete call: missing call list");
        handle_delete_context_free (ctx);
        return;
    }

    if (!mm_call_list_delete_call (list, ctx->path, &error))
        g_dbus_method_invocation_take_error (ctx->invocation, error);
    else
        mm_gdbus_modem_voice_complete_delete_call (ctx->skeleton, ctx->invocation);

    handle_delete_context_free (ctx);
    g_object_unref (list);
}

static gboolean
handle_delete (MmGdbusModemVoice *skeleton,
               GDBusMethodInvocation *invocation,
               const gchar *path,
               MMIfaceModemVoice *self)
{
    HandleDeleteContext *ctx;

    ctx = g_new (HandleDeleteContext, 1);
    ctx->skeleton = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self = g_object_ref (self);
    ctx->path = g_strdup (path);

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_VOICE,
                             (GAsyncReadyCallback)handle_delete_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice *self;
    GVariant *dictionary;
} HandleCreateContext;

static void
handle_create_context_free (HandleCreateContext *ctx)
{
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_variant_unref (ctx->dictionary);
    g_free (ctx);
}

static void
handle_create_auth_ready (MMBaseModem *self,
                          GAsyncResult *res,
                          HandleCreateContext *ctx)
{
    MMCallList *list = NULL;
    GError *error = NULL;
    MMCallProperties *properties;
    MMBaseCall *call;

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

    /* Parse input properties */
    properties = mm_call_properties_new_from_dictionary (ctx->dictionary, &error);
    if (!properties) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_create_context_free (ctx);
        return;
    }

    call = create_outgoing_call_from_properties (MM_IFACE_MODEM_VOICE (self), properties, &error);
    if (!call) {
        g_object_unref (properties);
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_create_context_free (ctx);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_object_unref (properties);
        g_object_unref (call);
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot create CALL: missing CALL list");
        handle_create_context_free (ctx);
        return;
    }

    /* Only export once properly created */
    mm_base_call_export (call);
    mm_call_list_add_call (list, call);

    /* Complete the DBus call */
    mm_gdbus_modem_voice_complete_create_call (ctx->skeleton,
                                               ctx->invocation,
                                               mm_base_call_get_path (call));
    g_object_unref (call);

    g_object_unref (properties);
    g_object_unref (list);

    handle_create_context_free (ctx);
}

static gboolean
handle_create (MmGdbusModemVoice *skeleton,
               GDBusMethodInvocation *invocation,
               GVariant *dictionary,
               MMIfaceModemVoice *self)
{
    HandleCreateContext *ctx;

    ctx = g_new (HandleCreateContext, 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_VOICE,
                             (GAsyncReadyCallback)handle_create_auth_ready,
                             ctx);
    return TRUE;
}

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

static gboolean
handle_list (MmGdbusModemVoice *skeleton,
             GDBusMethodInvocation *invocation,
             MMIfaceModemVoice *self)
{
    GStrv paths;
    MMCallList *list = NULL;

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot list CALL: missing CALL list");
        return TRUE;
    }

    paths = mm_call_list_get_paths (list);
    mm_gdbus_modem_voice_complete_list_calls (skeleton,
                                              invocation,
                                              (const gchar *const *)paths);
    g_strfreev (paths);
    g_object_unref (list);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    GList                 *active_calls;
    MMBaseCall            *next_call;
} HandleHoldAndAcceptContext;

static void
handle_hold_and_accept_context_free (HandleHoldAndAcceptContext *ctx)
{
    g_list_free_full (ctx->active_calls, g_object_unref);
    g_clear_object (&ctx->next_call);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleHoldAndAcceptContext, ctx);
}

static void
hold_and_accept_ready (MMIfaceModemVoice          *self,
                       GAsyncResult               *res,
                       HandleHoldAndAcceptContext *ctx)
{
    GError *error = NULL;
    GList  *l;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_hold_and_accept_context_free (ctx);
        return;
    }

    for (l = ctx->active_calls; l; l = g_list_next (l))
        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
    if (ctx->next_call)
        mm_base_call_change_state (ctx->next_call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);

    mm_gdbus_modem_voice_complete_hold_and_accept (ctx->skeleton, ctx->invocation);
    handle_hold_and_accept_context_free (ctx);
}

static void
prepare_hold_and_accept_foreach (MMBaseCall                 *call,
                                 HandleHoldAndAcceptContext *ctx)
{
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_ACTIVE:
        ctx->active_calls = g_list_append (ctx->active_calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_WAITING:
        g_clear_object (&ctx->next_call);
        ctx->next_call = g_object_ref (call);
        break;
    case MM_CALL_STATE_HELD:
        if (!ctx->next_call)
            ctx->next_call = g_object_ref (call);
        break;
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

static void
handle_hold_and_accept_auth_ready (MMBaseModem                *self,
                                   GAsyncResult               *res,
                                   HandleHoldAndAcceptContext *ctx)
{
    GError     *error = NULL;
    MMCallList *list = NULL;

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot hold and accept: unsupported");
        handle_hold_and_accept_context_free (ctx);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot hold and accept: missing call list");
        handle_hold_and_accept_context_free (ctx);
        return;
    }
    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hold_and_accept_foreach, ctx);
    g_object_unref (list);

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hold_and_accept (MM_IFACE_MODEM_VOICE (self),
                                                                (GAsyncReadyCallback)hold_and_accept_ready,
                                                                ctx);
}

static gboolean
handle_hold_and_accept (MmGdbusModemVoice     *skeleton,
                        GDBusMethodInvocation *invocation,
                        MMIfaceModemVoice     *self)
{
    HandleHoldAndAcceptContext *ctx;

    ctx = g_slice_new0 (HandleHoldAndAcceptContext);
    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_VOICE,
                             (GAsyncReadyCallback)handle_hold_and_accept_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    GList                 *active_calls;
    MMBaseCall            *next_call;
} HandleHangupAndAcceptContext;

static void
handle_hangup_and_accept_context_free (HandleHangupAndAcceptContext *ctx)
{
    g_list_free_full (ctx->active_calls, g_object_unref);
    g_clear_object (&ctx->next_call);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleHangupAndAcceptContext, ctx);
}

static void
hangup_and_accept_ready (MMIfaceModemVoice            *self,
                         GAsyncResult                 *res,
                         HandleHangupAndAcceptContext *ctx)
{
    GError *error = NULL;
    GList  *l;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_hangup_and_accept_context_free (ctx);
        return;
    }

    for (l = ctx->active_calls; l; l = g_list_next (l))
        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);
    if (ctx->next_call)
        mm_base_call_change_state (ctx->next_call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_ACCEPTED);

    mm_gdbus_modem_voice_complete_hangup_and_accept (ctx->skeleton, ctx->invocation);
    handle_hangup_and_accept_context_free (ctx);
}

static void
prepare_hangup_and_accept_foreach (MMBaseCall                   *call,
                                   HandleHangupAndAcceptContext *ctx)
{
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_ACTIVE:
        ctx->active_calls = g_list_append (ctx->active_calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_WAITING:
        g_clear_object (&ctx->next_call);
        ctx->next_call = g_object_ref (call);
        break;
    case MM_CALL_STATE_HELD:
        if (!ctx->next_call)
            ctx->next_call = g_object_ref (call);
        break;
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

static void
handle_hangup_and_accept_auth_ready (MMBaseModem                  *self,
                                     GAsyncResult                 *res,
                                     HandleHangupAndAcceptContext *ctx)
{
    GError     *error = NULL;
    MMCallList *list = NULL;

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot hangup and accept: unsupported");
        handle_hangup_and_accept_context_free (ctx);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot hangup and accept: missing call list");
        handle_hangup_and_accept_context_free (ctx);
        return;
    }
    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hangup_and_accept_foreach, ctx);
    g_object_unref (list);

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_and_accept (MM_IFACE_MODEM_VOICE (self),
                                                                  (GAsyncReadyCallback)hangup_and_accept_ready,
                                                                  ctx);
}

static gboolean
handle_hangup_and_accept (MmGdbusModemVoice     *skeleton,
                          GDBusMethodInvocation *invocation,
                          MMIfaceModemVoice     *self)
{
    HandleHangupAndAcceptContext *ctx;

    ctx = g_slice_new0 (HandleHangupAndAcceptContext);
    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_VOICE,
                             (GAsyncReadyCallback)handle_hangup_and_accept_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    GList                 *calls;
} HandleHangupAllContext;

static void
handle_hangup_all_context_free (HandleHangupAllContext *ctx)
{
    g_list_free_full (ctx->calls, g_object_unref);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleHangupAllContext, ctx);
}

static void
hangup_all_ready (MMIfaceModemVoice      *self,
                  GAsyncResult           *res,
                  HandleHangupAllContext *ctx)
{
    GError *error = NULL;
    GList  *l;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_hangup_all_context_free (ctx);
        return;
    }

    for (l = ctx->calls; l; l = g_list_next (l))
        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TERMINATED);

    mm_gdbus_modem_voice_complete_hangup_all (ctx->skeleton, ctx->invocation);
    handle_hangup_all_context_free (ctx);
}

static void
prepare_hangup_all_foreach (MMBaseCall             *call,
                            HandleHangupAllContext *ctx)
{
    /* The implementation of this operation will usually be done with +CHUP, and we
     * know that +CHUP is implemented in different ways by different manufacturers.
     *
     * The 3GPP TS27.007 spec for +CHUP states that the "Execution command causes
     * the TA to hangup the current call of the MT." This sentence leaves a bit of open
     * interpretation to the implementors, because a current call can be considered only
     * the active ones, or otherwise any call (active, held or waiting).
     *
     * And so, the u-blox TOBY-L4 takes one interpretation and "In case of multiple
     * calls, all active calls will be released, while waiting and held calls are not".
     *
     * And the Cinterion PLS-8 takes a different interpretation and cancels all calls,
     * including the waiting and held ones.
     *
     * In this logic, we're going to terminate exclusively the ACTIVE calls only, and we
     * will leave the possible termination of waiting/held calls to be reported via
     * call state updates, e.g. +CLCC polling or other plugin-specific method. In the
     * case of the Cinterion PLS-8, we'll detect the termination of the waiting and
     * held calls via ^SLCC URCs.
     */
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_ACTIVE:
        ctx->calls = g_list_append (ctx->calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_WAITING:
    case MM_CALL_STATE_HELD:
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

static void
handle_hangup_all_auth_ready (MMBaseModem            *self,
                              GAsyncResult           *res,
                              HandleHangupAllContext *ctx)
{
    GError     *error = NULL;
    MMCallList *list = NULL;

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot hangup all: unsupported");
        handle_hangup_all_context_free (ctx);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot hangup all: missing call list");
        handle_hangup_all_context_free (ctx);
        return;
    }
    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_hangup_all_foreach, ctx);
    g_object_unref (list);

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->hangup_all (MM_IFACE_MODEM_VOICE (self),
                                                           (GAsyncReadyCallback)hangup_all_ready,
                                                           ctx);
}

static gboolean
handle_hangup_all (MmGdbusModemVoice     *skeleton,
                   GDBusMethodInvocation *invocation,
                   MMIfaceModemVoice     *self)
{
    HandleHangupAllContext *ctx;

    ctx = g_slice_new0 (HandleHangupAllContext);
    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_VOICE,
                             (GAsyncReadyCallback)handle_hangup_all_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    GList                 *calls;
} HandleTransferContext;

static void
handle_transfer_context_free (HandleTransferContext *ctx)
{
    g_list_free_full (ctx->calls, g_object_unref);
    g_object_unref (ctx->skeleton);
    g_object_unref (ctx->invocation);
    g_object_unref (ctx->self);
    g_slice_free (HandleTransferContext, ctx);
}

static void
transfer_ready (MMIfaceModemVoice     *self,
                GAsyncResult          *res,
                HandleTransferContext *ctx)
{
    GError *error = NULL;
    GList  *l;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_transfer_context_free (ctx);
        return;
    }

    for (l = ctx->calls; l; l = g_list_next (l))
        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_TERMINATED, MM_CALL_STATE_REASON_TRANSFERRED);

    mm_gdbus_modem_voice_complete_transfer (ctx->skeleton, ctx->invocation);
    handle_transfer_context_free (ctx);
}

static void
prepare_transfer_foreach (MMBaseCall            *call,
                          HandleTransferContext *ctx)
{
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_ACTIVE:
    case MM_CALL_STATE_HELD:
        ctx->calls = g_list_append (ctx->calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_WAITING:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

static void
handle_transfer_auth_ready (MMBaseModem           *self,
                            GAsyncResult          *res,
                            HandleTransferContext *ctx)
{
    GError     *error = NULL;
    MMCallList *list = NULL;

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot transfer: unsupported");
        handle_transfer_context_free (ctx);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_WRONG_STATE,
                                               "Cannot transfer: missing call list");
        handle_transfer_context_free (ctx);
        return;
    }
    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_transfer_foreach, ctx);
    g_object_unref (list);

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->transfer (MM_IFACE_MODEM_VOICE (self),
                                                         (GAsyncReadyCallback)transfer_ready,
                                                         ctx);
}

static gboolean
handle_transfer (MmGdbusModemVoice     *skeleton,
                 GDBusMethodInvocation *invocation,
                 MMIfaceModemVoice     *self)
{
    HandleTransferContext *ctx;

    ctx = g_slice_new0 (HandleTransferContext);
    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_VOICE,
                             (GAsyncReadyCallback)handle_transfer_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    gboolean               enable;
} HandleCallWaitingSetupContext;

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

static void
call_waiting_setup_ready (MMIfaceModemVoice             *self,
                          GAsyncResult                  *res,
                          HandleCallWaitingSetupContext *ctx)
{
    GError *error = NULL;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup_finish (self, res, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_call_waiting_setup_context_free (ctx);
        return;
    }

    mm_gdbus_modem_voice_complete_call_waiting_setup (ctx->skeleton, ctx->invocation);
    handle_call_waiting_setup_context_free (ctx);
}

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

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot setup call waiting: unsupported");
        handle_call_waiting_setup_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_setup (MM_IFACE_MODEM_VOICE (self),
                                                                   ctx->enable,
                                                                   (GAsyncReadyCallback)call_waiting_setup_ready,
                                                                   ctx);
}

static gboolean
handle_call_waiting_setup (MmGdbusModemVoice     *skeleton,
                           GDBusMethodInvocation *invocation,
                           gboolean               enable,
                           MMIfaceModemVoice     *self)
{
    HandleCallWaitingSetupContext *ctx;

    ctx = g_slice_new0 (HandleCallWaitingSetupContext);
    ctx->skeleton   = g_object_ref (skeleton);
    ctx->invocation = g_object_ref (invocation);
    ctx->self       = g_object_ref (self);
    ctx->enable     = enable;

    mm_base_modem_authorize (MM_BASE_MODEM (self),
                             invocation,
                             MM_AUTHORIZATION_VOICE,
                             (GAsyncReadyCallback)handle_call_waiting_setup_auth_ready,
                             ctx);
    return TRUE;
}

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

typedef struct {
    MmGdbusModemVoice     *skeleton;
    GDBusMethodInvocation *invocation;
    MMIfaceModemVoice     *self;
    gboolean               enable;
} HandleCallWaitingQueryContext;

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

static void
call_waiting_query_ready (MMIfaceModemVoice             *self,
                          GAsyncResult                  *res,
                          HandleCallWaitingQueryContext *ctx)
{
    GError   *error = NULL;
    gboolean  status = FALSE;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query_finish (self, res, &status, &error)) {
        g_dbus_method_invocation_take_error (ctx->invocation, error);
        handle_call_waiting_query_context_free (ctx);
        return;
    }

    mm_gdbus_modem_voice_complete_call_waiting_query (ctx->skeleton, ctx->invocation, status);
    handle_call_waiting_query_context_free (ctx);
}

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

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

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query_finish) {
        g_dbus_method_invocation_return_error (ctx->invocation,
                                               MM_CORE_ERROR,
                                               MM_CORE_ERROR_UNSUPPORTED,
                                               "Cannot query call waiting: unsupported");
        handle_call_waiting_query_context_free (ctx);
        return;
    }

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->call_waiting_query (MM_IFACE_MODEM_VOICE (self),
                                                                   (GAsyncReadyCallback)call_waiting_query_ready,
                                                                   ctx);
}

static gboolean
handle_call_waiting_query (MmGdbusModemVoice     *skeleton,
                           GDBusMethodInvocation *invocation,
                           MMIfaceModemVoice     *self)
{
    HandleCallWaitingQueryContext *ctx;

    ctx = g_slice_new0 (HandleCallWaitingQueryContext);
    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_VOICE,
                             (GAsyncReadyCallback)handle_call_waiting_query_auth_ready,
                             ctx);
    return TRUE;
}

/*****************************************************************************/
/* Leave one of the calls from the multiparty call */

typedef struct {
    MMBaseCall *call;
    GList      *other_calls;
} LeaveMultipartyContext;

static void
leave_multiparty_context_free (LeaveMultipartyContext *ctx)
{
    g_list_free_full (ctx->other_calls, g_object_unref);
    g_object_unref (ctx->call);
    g_slice_free (LeaveMultipartyContext, ctx);
}

static void
prepare_leave_multiparty_foreach (MMBaseCall             *call,
                                  LeaveMultipartyContext *ctx)
{
    /* ignore call that is leaving */
    if ((call == ctx->call) || (g_strcmp0 (mm_base_call_get_path (call), mm_base_call_get_path (ctx->call)) == 0))
        return;

    /* ignore non-multiparty calls */
    if (!mm_base_call_get_multiparty (call))
        return;

    /* ignore calls not currently ongoing */
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_ACTIVE:
    case MM_CALL_STATE_HELD:
        ctx->other_calls = g_list_append (ctx->other_calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_WAITING:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

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

static void
leave_multiparty_ready (MMIfaceModemVoice *self,
                        GAsyncResult      *res,
                        GTask             *task)
{
    GError                 *error = NULL;
    LeaveMultipartyContext *ctx;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty_finish (self, res, &error)) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    /* If there is only one remaining call that was part of the multiparty, consider that
     * one also no longer part of any multiparty, and put it on hold right away */
    if (g_list_length (ctx->other_calls) == 1) {
        mm_base_call_set_multiparty (MM_BASE_CALL (ctx->other_calls->data), FALSE);
        mm_base_call_change_state (MM_BASE_CALL (ctx->other_calls->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
    }
    /* If there are still more than one calls in the multiparty, just change state of all
     * of them. */
    else {
        GList *l;

        for (l = ctx->other_calls; l; l = g_list_next (l))
            mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_HELD, MM_CALL_STATE_REASON_UNKNOWN);
    }

    /* The call that left would now be active */
    mm_base_call_set_multiparty (ctx->call, FALSE);
    mm_base_call_change_state (ctx->call, MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_UNKNOWN);

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

void
mm_iface_modem_voice_leave_multiparty (MMIfaceModemVoice   *self,
                                       MMBaseCall          *call,
                                       GAsyncReadyCallback  callback,
                                       gpointer             user_data)
{
    GTask                  *task;
    LeaveMultipartyContext *ctx;
    MMCallList             *list = NULL;
    MMCallState             call_state;

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

    /* validate multiparty status */
    if (!mm_base_call_get_multiparty (call)) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "this call is not part of a multiparty call");
        g_object_unref (task);
        return;
    }
    /* validate call state */
    call_state = mm_base_call_get_state (call);
    if ((call_state != MM_CALL_STATE_ACTIVE) && (call_state != MM_CALL_STATE_HELD)) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "invalid call state (%s): must be either active or held",
                                 mm_call_state_get_string (call_state));
        g_object_unref (task);
        return;
    }

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty_finish) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                 "Cannot leave multiparty: unsupported");
        g_object_unref (task);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "Cannot leave multiparty: missing call list");
        g_object_unref (task);
        return;
    }

    ctx = g_slice_new0 (LeaveMultipartyContext);
    ctx->call = g_object_ref (call);
    g_task_set_task_data (task, ctx, (GDestroyNotify) leave_multiparty_context_free);

    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_leave_multiparty_foreach, ctx);
    g_object_unref (list);

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->leave_multiparty (self,
                                                                 call,
                                                                 (GAsyncReadyCallback)leave_multiparty_ready,
                                                                 task);
}

/*****************************************************************************/
/* Join calls into a multiparty call */

typedef struct {
    MMBaseCall *call;
    GList      *all_calls;
    gboolean    added;
} JoinMultipartyContext;

static void
join_multiparty_context_free (JoinMultipartyContext *ctx)
{
    g_list_free_full (ctx->all_calls, g_object_unref);
    g_object_unref (ctx->call);
    g_slice_free (JoinMultipartyContext, ctx);
}

static void
prepare_join_multiparty_foreach (MMBaseCall            *call,
                                 JoinMultipartyContext *ctx)
{
    /* always add call that is being added */
    if ((call == ctx->call) || (g_strcmp0 (mm_base_call_get_path (call), mm_base_call_get_path (ctx->call)) == 0))
        ctx->added = TRUE;

    /* ignore calls not currently ongoing */
    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_ACTIVE:
    case MM_CALL_STATE_HELD:
        ctx->all_calls = g_list_append (ctx->all_calls, g_object_ref (call));
        break;
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_WAITING:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

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

static void
join_multiparty_ready (MMIfaceModemVoice *self,
                       GAsyncResult      *res,
                       GTask             *task)
{
    GError                *error = NULL;
    JoinMultipartyContext *ctx;
    GList                 *l;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty_finish (self, res, &error)) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

    for (l = ctx->all_calls; l; l = g_list_next (l)) {
        mm_base_call_set_multiparty (MM_BASE_CALL (l->data), TRUE);
        mm_base_call_change_state (MM_BASE_CALL (l->data), MM_CALL_STATE_ACTIVE, MM_CALL_STATE_REASON_UNKNOWN);
    }

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

void
mm_iface_modem_voice_join_multiparty (MMIfaceModemVoice   *self,
                                      MMBaseCall          *call,
                                      GAsyncReadyCallback  callback,
                                      gpointer             user_data)
{
    GTask                 *task;
    JoinMultipartyContext *ctx;
    MMCallList            *list = NULL;
    MMCallState            call_state;

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

    /* validate multiparty status */
    if (mm_base_call_get_multiparty (call)) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "this call is already part of a multiparty call");
        g_object_unref (task);
        return;
    }
    /* validate call state */
    call_state = mm_base_call_get_state (call);
    if (call_state != MM_CALL_STATE_HELD) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "invalid call state (%s): must be held",
                                 mm_call_state_get_string (call_state));
        g_object_unref (task);
        return;
    }

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty ||
        !MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty_finish) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED,
                                 "Cannot join multiparty: unsupported");
        g_object_unref (task);
        return;
    }

    g_object_get (self,
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_WRONG_STATE,
                                 "Cannot join multiparty: missing call list");
        g_object_unref (task);
        return;
    }

    ctx = g_slice_new0 (JoinMultipartyContext);
    ctx->call = g_object_ref (call);
    g_task_set_task_data (task, ctx, (GDestroyNotify) join_multiparty_context_free);

    mm_call_list_foreach (list, (MMCallListForeachFunc)prepare_join_multiparty_foreach, ctx);
    g_object_unref (list);

    /* our logic makes sure that we would be adding the incoming call into the multiparty call */
    g_assert (ctx->added);

    /* NOTE: we do not give the call we want to join, because the join operation acts on all
     * active/held calls. */
    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->join_multiparty (self,
                                                                (GAsyncReadyCallback)join_multiparty_ready,
                                                                task);
}

/*****************************************************************************/
/* In-call setup operation
 *
 * It will setup URC handlers for all in-call URCs, and also setup the audio
 * channel if the plugin requires to do so.
 */

typedef enum {
    IN_CALL_SETUP_STEP_FIRST,
    IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS,
    IN_CALL_SETUP_STEP_AUDIO_CHANNEL,
    IN_CALL_SETUP_STEP_LAST,
} InCallSetupStep;

typedef struct {
    InCallSetupStep    step;
    MMPort            *audio_port;
    MMCallAudioFormat *audio_format;
} InCallSetupContext;

static void
in_call_setup_context_free (InCallSetupContext *ctx)
{
    g_clear_object (&ctx->audio_port);
    g_clear_object (&ctx->audio_format);
    g_slice_free (InCallSetupContext, ctx);
}

static gboolean
in_call_setup_finish (MMIfaceModemVoice  *self,
                      GAsyncResult       *res,
                      MMPort            **audio_port,   /* optional */
                      MMCallAudioFormat **audio_format, /* optional */
                      GError            **error)
{
    InCallSetupContext *ctx;

    if (!g_task_propagate_boolean (G_TASK (res), error))
        return FALSE;

    ctx = g_task_get_task_data (G_TASK (res));
    if (audio_port) {
        *audio_port = ctx->audio_port;
        ctx->audio_port = NULL;
    }
    if (audio_format) {
        *audio_format = ctx->audio_format;
        ctx->audio_format = NULL;
    }

    return TRUE;
}

static void in_call_setup_context_step (GTask *task);

static void
setup_in_call_audio_channel_ready (MMIfaceModemVoice *self,
                                   GAsyncResult      *res,
                                   GTask             *task)
{
    InCallSetupContext *ctx;
    GError             *error = NULL;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish (self,
                                                                                        res,
                                                                                        &ctx->audio_port,
                                                                                        &ctx->audio_format,
                                                                                        &error)) {
        mm_obj_warn (self, "couldn't setup in-call audio channel: %s", error->message);
        g_clear_error (&error);
    }

    ctx->step++;
    in_call_setup_context_step (task);
}

static void
setup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self,
                                        GAsyncResult      *res,
                                        GTask             *task)
{
    InCallSetupContext *ctx;
    GError             *error = NULL;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish (self, res, &error)) {
        mm_obj_warn (self, "couldn't setup in-call unsolicited events: %s", error->message);
        g_clear_error (&error);
    }

    ctx->step++;
    in_call_setup_context_step (task);
}

static void
in_call_setup_context_step (GTask *task)
{
    MMIfaceModemVoice  *self;
    InCallSetupContext *ctx;

    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 IN_CALL_SETUP_STEP_FIRST:
        ctx->step++;
        /* fall-through */
    case IN_CALL_SETUP_STEP_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_unsolicited_events (
                self,
                (GAsyncReadyCallback) setup_in_call_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall-through */
    case IN_CALL_SETUP_STEP_AUDIO_CHANNEL:
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_in_call_audio_channel (
                self,
                (GAsyncReadyCallback) setup_in_call_audio_channel_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall-through */
    case IN_CALL_SETUP_STEP_LAST:
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        break;
    }

    g_assert_not_reached ();
}

static void
in_call_setup (MMIfaceModemVoice   *self,
               GCancellable        *cancellable,
               GAsyncReadyCallback  callback,
               gpointer             user_data)
{
    GTask              *task;
    InCallSetupContext *ctx;

    ctx = g_slice_new0 (InCallSetupContext);
    ctx->step = IN_CALL_SETUP_STEP_FIRST;

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

    in_call_setup_context_step (task);
}

/*****************************************************************************/
/* In-call cleanup operation
 *
 * It will cleanup audio channel settings and remove all in-call URC handlers.
 */

typedef enum {
    IN_CALL_CLEANUP_STEP_FIRST,
    IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL,
    IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS,
    IN_CALL_CLEANUP_STEP_LAST,
} InCallCleanupStep;

typedef struct {
    InCallCleanupStep step;
} InCallCleanupContext;

static gboolean
in_call_cleanup_finish (MMIfaceModemVoice  *self,
                        GAsyncResult       *res,
                        GError            **error)
{
    return g_task_propagate_boolean (G_TASK (res), error);
}

static void in_call_cleanup_context_step (GTask *task);

static void
cleanup_in_call_unsolicited_events_ready (MMIfaceModemVoice *self,
                                          GAsyncResult      *res,
                                          GTask             *task)
{
    InCallCleanupContext *ctx;
    GError             *error = NULL;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish (self, res, &error)) {
        mm_obj_warn (self, "couldn't cleanup in-call unsolicited events: %s", error->message);
        g_clear_error (&error);
    }

    ctx->step++;
    in_call_cleanup_context_step (task);
}

static void
cleanup_in_call_audio_channel_ready (MMIfaceModemVoice *self,
                                     GAsyncResult      *res,
                                     GTask             *task)
{
    InCallCleanupContext *ctx;
    GError             *error = NULL;

    ctx = g_task_get_task_data (task);

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish (self, res, &error)) {
        mm_obj_warn (self, "couldn't cleanup in-call audio channel: %s", error->message);
        g_clear_error (&error);
    }

    ctx->step++;
    in_call_cleanup_context_step (task);
}

static void
in_call_cleanup_context_step (GTask *task)
{
    MMIfaceModemVoice    *self;
    InCallCleanupContext *ctx;

    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 IN_CALL_CLEANUP_STEP_FIRST:
        ctx->step++;
        /* fall-through */
    case IN_CALL_CLEANUP_STEP_AUDIO_CHANNEL:
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_audio_channel (
                self,
                (GAsyncReadyCallback) cleanup_in_call_audio_channel_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall-through */
    case IN_CALL_CLEANUP_STEP_UNSOLICITED_EVENTS:
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_in_call_unsolicited_events (
                self,
                (GAsyncReadyCallback) cleanup_in_call_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall-through */
    case IN_CALL_CLEANUP_STEP_LAST:
        g_task_return_boolean (task, TRUE);
        g_object_unref (task);
        return;

    default:
        break;
    }

    g_assert_not_reached ();
}

static void
in_call_cleanup (MMIfaceModemVoice   *self,
                 GCancellable        *cancellable,
                 GAsyncReadyCallback  callback,
                 gpointer             user_data)
{
    GTask                *task;
    InCallCleanupContext *ctx;

    ctx = g_new0 (InCallCleanupContext, 1);
    ctx->step = IN_CALL_CLEANUP_STEP_FIRST;

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

    in_call_cleanup_context_step (task);
}

/*****************************************************************************/
/* In-call event handling logic
 *
 * This procedure will run a in-call setup async function whenever we detect
 * that there is at least one call that is ongoing. This setup function will
 * try to setup in-call unsolicited events as well as any audio channel
 * requirements.
 *
 * The procedure will run a in-call cleanup async function whenever we detect
 * that there are no longer any ongoing calls. The cleanup function will
 * cleanup the audio channel and remove the in-call unsolicited event handlers.
 */

typedef struct {
    guint              check_id;
    GCancellable      *setup_cancellable;
    GCancellable      *cleanup_cancellable;
    gboolean           in_call_state;
    MMPort            *audio_port;
    MMCallAudioFormat *audio_format;
} InCallEventContext;

static void
in_call_event_context_free (InCallEventContext *ctx)
{
    if (ctx->check_id)
        g_source_remove (ctx->check_id);
    if (ctx->cleanup_cancellable) {
        g_cancellable_cancel (ctx->cleanup_cancellable);
        g_clear_object (&ctx->cleanup_cancellable);
    }
    if (ctx->setup_cancellable) {
        g_cancellable_cancel (ctx->setup_cancellable);
        g_clear_object (&ctx->setup_cancellable);
    }
    g_clear_object (&ctx->audio_port);
    g_clear_object (&ctx->audio_format);
    g_slice_free (InCallEventContext, ctx);
}

static InCallEventContext *
get_in_call_event_context (MMIfaceModemVoice *self)
{
    InCallEventContext *ctx;

    if (G_UNLIKELY (!in_call_event_context_quark))
        in_call_event_context_quark = g_quark_from_static_string (IN_CALL_EVENT_CONTEXT_TAG);

    ctx = g_object_get_qdata (G_OBJECT (self), in_call_event_context_quark);
    if (!ctx) {
        /* Create context and keep it as object data */
        ctx = g_slice_new0 (InCallEventContext);
        g_object_set_qdata_full (
            G_OBJECT (self),
            in_call_event_context_quark,
            ctx,
            (GDestroyNotify)in_call_event_context_free);
    }

    return ctx;
}

static void
call_list_foreach_audio_settings (MMBaseCall         *call,
                                  InCallEventContext *ctx)
{
    if (mm_base_call_get_state (call) != MM_CALL_STATE_TERMINATED)
        return;
    mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format);
}

static void
update_audio_settings_in_ongoing_calls (MMIfaceModemVoice *self)
{
    MMCallList         *list = NULL;
    InCallEventContext *ctx;

    ctx = get_in_call_event_context (self);

    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        mm_obj_warn (self, "cannot update audio settings in active calls: missing internal call list");
        return;
    }

    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_audio_settings, ctx);
    g_clear_object (&list);
}

static void
update_audio_settings_in_call (MMIfaceModemVoice *self,
                               MMBaseCall        *call)
{
    InCallEventContext *ctx;

    ctx = get_in_call_event_context (self);
    mm_base_call_change_audio_settings (call, ctx->audio_port, ctx->audio_format);
}

static void
call_list_foreach_count_in_call (MMBaseCall *call,
                                 gpointer    user_data)
{
    guint *n_calls_in_call = (guint *)user_data;

    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_HELD:
    case MM_CALL_STATE_ACTIVE:
        *n_calls_in_call = *n_calls_in_call + 1;
        break;
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_WAITING:
        /* NOTE: ringing-in and waiting calls are NOT yet in-call, e.g. there must
         * be no audio settings enabled and we must not enable in-call URC handling
         * yet. */
    case MM_CALL_STATE_UNKNOWN:
    case MM_CALL_STATE_TERMINATED:
    default:
        break;
    }
}

static void
in_call_cleanup_ready (MMIfaceModemVoice *self,
                       GAsyncResult      *res)
{
    GError             *error = NULL;
    InCallEventContext *ctx;

    ctx = get_in_call_event_context (self);

    if (!in_call_cleanup_finish (self, res, &error)) {
        /* ignore cancelled operations */
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            mm_obj_warn (self, "cannot cleanup in-call modem state: %s", error->message);
        g_clear_error (&error);
    } else {
        mm_obj_dbg (self, "modem is no longer in-call state");
        ctx->in_call_state = FALSE;
        g_clear_object (&ctx->audio_port);
        g_clear_object (&ctx->audio_format);
    }

    g_clear_object (&ctx->cleanup_cancellable);
}

static void
in_call_setup_ready (MMIfaceModemVoice *self,
                     GAsyncResult      *res)
{
    GError             *error = NULL;
    InCallEventContext *ctx;

    ctx = get_in_call_event_context (self);

    if (!in_call_setup_finish (self, res, &ctx->audio_port, &ctx->audio_format, &error)) {
        /* ignore cancelled operations */
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            mm_obj_warn (self, "cannot setup in-call modem state: %s", error->message);
        g_clear_error (&error);
    } else {
        mm_obj_dbg (self, "modem is now in-call state");
        ctx->in_call_state = TRUE;
        update_audio_settings_in_ongoing_calls (self);
    }

    g_clear_object (&ctx->setup_cancellable);
}

static gboolean
call_list_check_in_call_events (MMIfaceModemVoice *self)
{
    InCallEventContext *ctx;
    MMCallList         *list = NULL;
    guint               n_calls_in_call = 0;

    ctx = get_in_call_event_context (self);
    ctx->check_id = 0;

    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);
    if (!list) {
        mm_obj_warn (self, "cannot update in-call state: missing internal call list");
        goto out;
    }

    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_count_in_call, &n_calls_in_call);

    /* Need to setup in-call events? */
    if (n_calls_in_call > 0 && !ctx->in_call_state) {
        /* if setup already ongoing, do nothing */
        if (ctx->setup_cancellable)
            goto out;

        /* cancel ongoing cleanup if any */
        if (ctx->cleanup_cancellable) {
            g_cancellable_cancel (ctx->cleanup_cancellable);
            g_clear_object (&ctx->cleanup_cancellable);
        }

        /* run setup */
        mm_obj_dbg (self, "setting up in-call state...");
        ctx->setup_cancellable = g_cancellable_new ();
        in_call_setup (self, ctx->setup_cancellable, (GAsyncReadyCallback) in_call_setup_ready, NULL);
        goto out;
    }

    /* Need to cleanup in-call events? */
    if (n_calls_in_call == 0 && ctx->in_call_state) {
        /* if cleanup already ongoing, do nothing */
        if (ctx->cleanup_cancellable)
            goto out;

        /* cancel ongoing setup if any */
        if (ctx->setup_cancellable) {
            g_cancellable_cancel (ctx->setup_cancellable);
            g_clear_object (&ctx->setup_cancellable);
        }

        /* run cleanup */
        mm_obj_dbg (self, "cleaning up in-call state...");
        ctx->cleanup_cancellable = g_cancellable_new ();
        in_call_cleanup (self, ctx->cleanup_cancellable, (GAsyncReadyCallback) in_call_cleanup_ready, NULL);
        goto out;
    }

 out:
    g_clear_object (&list);
    return G_SOURCE_REMOVE;
}

static void
call_state_changed (MMIfaceModemVoice *self)
{
    InCallEventContext *ctx;

    ctx = get_in_call_event_context (self);
    if (ctx->check_id)
        return;

    /* Process check for in-call events in an idle, so that we can combine
     * together in the same check multiple call state updates happening
     * at the same time for different calls (e.g. when swapping active/held
     * calls). */
    ctx->check_id = g_idle_add ((GSourceFunc)call_list_check_in_call_events, self);
}

static void
setup_in_call_event_handling (MMCallList        *call_list,
                              const gchar       *call_path_added,
                              MMIfaceModemVoice *self)
{
    MMBaseCall *call;

    call = mm_call_list_get_call (call_list, call_path_added);
    g_assert (call);

    g_signal_connect_swapped (call,
                              "state-changed",
                              G_CALLBACK (call_state_changed),
                              self);
}

/*****************************************************************************/
/* Call list polling logic
 *
 * The call list polling is exclusively used to detect detailed call state
 * updates while a call is being established. Therefore, if there is no call
 * being established (i.e. all terminated, unknown or active), then there is
 * no polling to do.
 *
 * Any time we add a new call to the list, we'll setup polling if it's not
 * already running, and the polling logic itself will decide when the polling
 * should stop.
 */

#define CALL_LIST_POLLING_TIMEOUT_SECS 2

typedef struct {
    guint    polling_id;
    gboolean polling_ongoing;
} CallListPollingContext;

static void
call_list_polling_context_free (CallListPollingContext *ctx)
{
    if (ctx->polling_id)
        g_source_remove (ctx->polling_id);
    g_slice_free (CallListPollingContext, ctx);
}

static CallListPollingContext *
get_call_list_polling_context (MMIfaceModemVoice *self)
{
    CallListPollingContext *ctx;

    if (G_UNLIKELY (!call_list_polling_context_quark))
        call_list_polling_context_quark =  (g_quark_from_static_string (
                                                 CALL_LIST_POLLING_CONTEXT_TAG));

    ctx = g_object_get_qdata (G_OBJECT (self), call_list_polling_context_quark);
    if (!ctx) {
        /* Create context and keep it as object data */
        ctx = g_slice_new0 (CallListPollingContext);

        g_object_set_qdata_full (
            G_OBJECT (self),
            call_list_polling_context_quark,
            ctx,
            (GDestroyNotify)call_list_polling_context_free);
    }

    return ctx;
}

static gboolean call_list_poll (MMIfaceModemVoice *self);

static void
load_call_list_ready (MMIfaceModemVoice *self,
                      GAsyncResult      *res)
{
    CallListPollingContext *ctx;
    GList                  *call_info_list = NULL;
    GError                 *error = NULL;

    ctx = get_call_list_polling_context (self);
    ctx->polling_ongoing = FALSE;

    g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish);
    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish (self, res, &call_info_list, &error)) {
        mm_obj_warn (self, "couldn't load call list: %s", error->message);
        g_error_free (error);
    } else {
        /* Always report the list even if NULL (it would mean no ongoing calls) */
        mm_iface_modem_voice_report_all_calls (self, call_info_list);
        mm_3gpp_call_info_list_free (call_info_list);
    }

    /* setup the polling again, but only if it hasn't been done already while
     * we reported calls (e.g. a new incoming call may have been detected that
     * also triggers the poll setup) */
    if (!ctx->polling_id)
        ctx->polling_id = g_timeout_add_seconds (CALL_LIST_POLLING_TIMEOUT_SECS,
                                                 (GSourceFunc) call_list_poll,
                                                 self);
}

static void
call_list_foreach_count_establishing (MMBaseCall *call,
                                      gpointer    user_data)
{
    guint *n_calls_establishing = (guint *)user_data;

    switch (mm_base_call_get_state (call)) {
    case MM_CALL_STATE_DIALING:
    case MM_CALL_STATE_RINGING_OUT:
    case MM_CALL_STATE_RINGING_IN:
    case MM_CALL_STATE_HELD:
    case MM_CALL_STATE_WAITING:
        *n_calls_establishing = *n_calls_establishing + 1;
        break;
    case MM_CALL_STATE_ACTIVE:
    case MM_CALL_STATE_TERMINATED:
    case MM_CALL_STATE_UNKNOWN:
    default:
        break;
    }
}

static gboolean
call_list_poll (MMIfaceModemVoice *self)
{
    CallListPollingContext *ctx;
    MMCallList             *list = NULL;
    guint                   n_calls_establishing = 0;

    ctx = get_call_list_polling_context (self);
    ctx->polling_id = 0;

    g_object_get (MM_BASE_MODEM (self),
                  MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                  NULL);

    if (!list) {
        mm_obj_warn (self, "Cannot poll call list: missing internal call list");
        goto out;
    }

    mm_call_list_foreach (list, (MMCallListForeachFunc) call_list_foreach_count_establishing, &n_calls_establishing);

    /* If there is at least ONE call being established, we need the call list */
    if (n_calls_establishing > 0) {
        mm_obj_dbg (self, "%u calls being established: call list polling required", n_calls_establishing);
        ctx->polling_ongoing = TRUE;
        g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list);
        MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list (self,
                                                                   (GAsyncReadyCallback)load_call_list_ready,
                                                                   NULL);
    } else
        mm_obj_dbg (self, "no calls being established: call list polling stopped");

out:
    g_clear_object (&list);
    return G_SOURCE_REMOVE;
}

static void
setup_call_list_polling (MMCallList        *call_list,
                         const gchar       *call_path_added,
                         MMIfaceModemVoice *self)
{
    CallListPollingContext *ctx;

    ctx = get_call_list_polling_context (self);

    if (!ctx->polling_id && !ctx->polling_ongoing)
        ctx->polling_id = g_timeout_add_seconds (CALL_LIST_POLLING_TIMEOUT_SECS,
                                                 (GSourceFunc) call_list_poll,
                                                 self);
}

/*****************************************************************************/
/* Call list reload */

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

static void
reload_all_calls_ready (MMIfaceModemVoice *self,
                        GAsyncResult      *res,
                        GTask             *task)
{
    GList  *call_info_list = NULL;
    GError *error = NULL;

    g_assert (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish);
    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish (self, res, &call_info_list, &error)) {
        mm_obj_warn (self, "couldn't reload call list: %s", error->message);

        g_task_return_error (task, error);
    } else {
        /* Always report the list even if NULL (it would mean no ongoing calls) */
        mm_iface_modem_voice_report_all_calls (self, call_info_list);
        mm_3gpp_call_info_list_free (call_info_list);

        g_task_return_boolean (task, TRUE);
    }

    g_object_unref (task);
}

void
mm_iface_modem_voice_reload_all_calls (MMIfaceModemVoice   *self,
                                       GAsyncReadyCallback  callback,
                                       gpointer             user_data)
{
    GTask *task;

    task = g_task_new (self, NULL, callback, user_data);
    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list (self,
                                                               (GAsyncReadyCallback)reload_all_calls_ready,
                                                               task);
}

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

static void
update_call_list (MmGdbusModemVoice *skeleton,
                  MMCallList        *list)
{
    gchar **paths;

    paths = mm_call_list_get_paths (list);
    mm_gdbus_modem_voice_set_calls (skeleton, (const gchar *const *)paths);
    g_strfreev (paths);

    g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (skeleton));
}

static void
call_added (MMCallList        *list,
            const gchar       *call_path,
            MmGdbusModemVoice *skeleton)
{
    update_call_list (skeleton, list);
    mm_gdbus_modem_voice_emit_call_added (skeleton, call_path);
}

static void
call_deleted (MMCallList        *list,
              const gchar       *call_path,
              MmGdbusModemVoice *skeleton)
{
    update_call_list (skeleton, list);
    mm_gdbus_modem_voice_emit_call_deleted (skeleton, call_path);
}

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

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

typedef enum {
    DISABLING_STEP_FIRST,
    DISABLING_STEP_DISABLE_UNSOLICITED_EVENTS,
    DISABLING_STEP_CLEANUP_UNSOLICITED_EVENTS,
    DISABLING_STEP_LAST
} DisablingStep;

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

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

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

static void
disable_unsolicited_events_ready (MMIfaceModemVoice *self,
                                  GAsyncResult *res,
                                  GTask *task)
{
    DisablingContext *ctx;
    GError *error = NULL;

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->disable_unsolicited_events_finish (self, res, &error);
    if (error) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

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

static void
cleanup_unsolicited_events_ready (MMIfaceModemVoice *self,
                                  GAsyncResult *res,
                                  GTask *task)
{
    DisablingContext *ctx;
    GError *error = NULL;

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_unsolicited_events_finish (self, res, &error);
    if (error) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

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

static void
interface_disabling_step (GTask *task)
{
    MMIfaceModemVoice *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_DISABLE_UNSOLICITED_EVENTS:
        /* Allow cleaning up unsolicited events */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->disable_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->disable_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->disable_unsolicited_events (
                self,
                (GAsyncReadyCallback)disable_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case DISABLING_STEP_CLEANUP_UNSOLICITED_EVENTS:
        /* Allow cleaning up unsolicited events */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->cleanup_unsolicited_events (
                self,
                (GAsyncReadyCallback)cleanup_unsolicited_events_ready,
                task);
            return;
        }
        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:
        break;
    }

    g_assert_not_reached ();
}

void
mm_iface_modem_voice_disable (MMIfaceModemVoice *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_VOICE_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_LAST
} EnablingStep;

struct _EnablingContext {
    EnablingStep step;
    MmGdbusModemVoice *skeleton;
    guint mem1_storage_index;
};

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

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

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

    MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_unsolicited_events_finish (self, res, &error);
    if (error) {
        g_task_return_error (task, error);
        g_object_unref (task);
        return;
    }

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

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

    /* Not critical! */
    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->enable_unsolicited_events_finish (self, res, &error)) {
        mm_obj_dbg (self, "couldn't enable unsolicited events: %s", error->message);
        g_error_free (error);
    }

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

static void
interface_enabling_step (GTask *task)
{
    MMIfaceModemVoice *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:
        /* Allow setting up unsolicited events to get notified of incoming calls */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->setup_unsolicited_events (
                self,
                (GAsyncReadyCallback)setup_unsolicited_events_ready,
                task);
            return;
        }
        ctx->step++;
        /* fall through */

    case ENABLING_STEP_ENABLE_UNSOLICITED_EVENTS:
        /* Allow setting up unsolicited events to get notified of incoming calls */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->enable_unsolicited_events &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->enable_unsolicited_events_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->enable_unsolicited_events (
                self,
                (GAsyncReadyCallback)enable_unsolicited_events_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_voice_enable (MMIfaceModemVoice *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_VOICE_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);
}

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

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

typedef enum {
    INITIALIZATION_STEP_FIRST,
    INITIALIZATION_STEP_CHECK_SUPPORT,
    INITIALIZATION_STEP_SETUP_CALL_LIST,
    INITIALIZATION_STEP_LAST
} InitializationStep;

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

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

static void
check_support_ready (MMIfaceModemVoice *self,
                     GAsyncResult *res,
                     GTask *task)
{
    InitializationContext *ctx;
    GError *error = NULL;

    if (!MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->check_support_finish (self, res, &error)) {
        if (error) {
            mm_obj_dbg (self, "voice support check failed: %s", error->message);
            g_error_free (error);
        }
        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED, "Voice not supported");
        g_object_unref (task);
        return;
    }

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

static void
interface_initialization_step (GTask *task)
{
    MMIfaceModemVoice *self;
    InitializationContext *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 INITIALIZATION_STEP_FIRST:
        ctx->step++;
        /* fall through */

    case INITIALIZATION_STEP_CHECK_SUPPORT:
        /* Always check voice support when we run initialization, because
         * the support may be different before and after SIM-PIN unlock. */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->check_support &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->check_support_finish) {
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->check_support (
                self,
                (GAsyncReadyCallback)check_support_ready,
                task);
            return;
        }

        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED, "Voice not supported");
        g_object_unref (task);
        return;

    case INITIALIZATION_STEP_SETUP_CALL_LIST: {
        MMCallList *list = NULL;

        g_object_get (self,
                      MM_IFACE_MODEM_VOICE_CALL_LIST, &list,
                      NULL);

        /* Create a new call list if not already available (this initialization
         * may be called multiple times) */
        if (!list) {
            list = mm_call_list_new (MM_BASE_MODEM (self));
            g_object_set (self,
                          MM_IFACE_MODEM_VOICE_CALL_LIST, list,
                          NULL);

            /* Connect to list's signals */
            g_signal_connect (list,
                              MM_CALL_ADDED,
                              G_CALLBACK (call_added),
                              ctx->skeleton);
            g_signal_connect (list,
                              MM_CALL_DELETED,
                              G_CALLBACK (call_deleted),
                              ctx->skeleton);

            /* Setup monitoring for in-call event handling */
            g_signal_connect (list,
                              MM_CALL_ADDED,
                              G_CALLBACK (setup_in_call_event_handling),
                              self);
        }

        /* Unless we're told not to, setup call list polling logic */
        if (MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list &&
            MM_IFACE_MODEM_VOICE_GET_INTERFACE (self)->load_call_list_finish) {
            gboolean periodic_call_list_check_disabled = FALSE;

            /* Cleanup any previously configured handler, before checking if we need to
             * add a new one, because the PERIODIC_CALL_LIST_CHECK_DISABLED flag may
             * change before and after SIM-PIN unlock */
            g_signal_handlers_disconnect_by_func (list, G_CALLBACK (setup_call_list_polling), self);

            g_object_get (self,
                          MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED, &periodic_call_list_check_disabled,
                          NULL);
            if (!periodic_call_list_check_disabled) {
                mm_obj_dbg (self, "periodic call list polling will be used if supported");
                g_signal_connect (list,
                                  MM_CALL_ADDED,
                                  G_CALLBACK (setup_call_list_polling),
                                  self);
            }
        }
        g_object_unref (list);

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

    case INITIALIZATION_STEP_LAST:
        /* Setup all method handlers */
        g_object_connect (ctx->skeleton,
                          "signal::handle-create-call",        G_CALLBACK (handle_create),             self,
                          "signal::handle-delete-call",        G_CALLBACK (handle_delete),             self,
                          "signal::handle-list-calls",         G_CALLBACK (handle_list),               self,
                          "signal::handle-hangup-and-accept",  G_CALLBACK (handle_hangup_and_accept),  self,
                          "signal::handle-hold-and-accept",    G_CALLBACK (handle_hold_and_accept),    self,
                          "signal::handle-hangup-all",         G_CALLBACK (handle_hangup_all),         self,
                          "signal::handle-transfer",           G_CALLBACK (handle_transfer),           self,
                          "signal::handle-call-waiting-setup", G_CALLBACK (handle_call_waiting_setup), self,
                          "signal::handle-call-waiting-query", G_CALLBACK (handle_call_waiting_query), self,
                          NULL);

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

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

    default:
        break;
    }

    g_assert_not_reached ();
}

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

static gboolean
modem_state_to_emergency_only (GBinding     *binding,
                               const GValue *from_value,
                               GValue       *to_value)
{
    MMModemState state;

    /* If the modem is REGISTERED, we allow any kind of call, otherwise
     * only emergency calls */
    state = g_value_get_enum (from_value);
    g_value_set_boolean (to_value, (state < MM_MODEM_STATE_REGISTERED));
    return TRUE;
}

void
mm_iface_modem_voice_initialize (MMIfaceModemVoice *self,
                                 GCancellable *cancellable,
                                 GAsyncReadyCallback callback,
                                 gpointer user_data)
{
    InitializationContext *ctx;
    MmGdbusModemVoice *skeleton = NULL;
    GTask *task;

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

        g_object_set (self,
                      MM_IFACE_MODEM_VOICE_DBUS_SKELETON, skeleton,
                      NULL);

        g_object_bind_property_full (self, MM_IFACE_MODEM_STATE,
                                     skeleton, "emergency-only",
                                     G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE,
                                     (GBindingTransformFunc) modem_state_to_emergency_only,
                                     NULL, NULL, NULL);
    }

    /* Perform async initialization here */

    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);

    interface_initialization_step (task);
}

void
mm_iface_modem_voice_shutdown (MMIfaceModemVoice *self)
{
    /* Unexport DBus interface and remove the skeleton */
    mm_gdbus_object_skeleton_set_modem_voice (MM_GDBUS_OBJECT_SKELETON (self), NULL);
    g_object_set (self,
                  MM_IFACE_MODEM_VOICE_DBUS_SKELETON, NULL,
                  NULL);
}

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

static void
iface_modem_voice_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_VOICE_DBUS_SKELETON,
                              "Voice DBus skeleton",
                              "DBus skeleton for the Voice interface",
                              MM_GDBUS_TYPE_MODEM_VOICE_SKELETON,
                              G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_object (MM_IFACE_MODEM_VOICE_CALL_LIST,
                              "CALL list",
                              "List of CALL objects managed in the interface",
                              MM_TYPE_CALL_LIST,
                              G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED,
                               "Periodic call list checks disabled",
                               "Whether periodic call list check are disabled.",
                               FALSE,
                               G_PARAM_READWRITE));

    g_object_interface_install_property
        (g_iface,
         g_param_spec_boolean (MM_IFACE_MODEM_VOICE_INDICATION_CALL_LIST_RELOAD_ENABLED,
                               "Reload call list on call update",
                               "Ignore call updates and forcefully reload all calls.",
                               FALSE,
                               G_PARAM_READWRITE));

    initialized = TRUE;
}

GType
mm_iface_modem_voice_get_type (void)
{
    static GType iface_modem_voice_type = 0;

    if (!G_UNLIKELY (iface_modem_voice_type)) {
        static const GTypeInfo info = {
            sizeof (MMIfaceModemVoice), /* class_size */
            iface_modem_voice_init,     /* base_init */
            NULL,                           /* base_finalize */
        };

        iface_modem_voice_type = g_type_register_static (G_TYPE_INTERFACE,
                                                         "MMIfaceModemVoice",
                                                         &info,
                                                         0);

        g_type_interface_add_prerequisite (iface_modem_voice_type, MM_TYPE_IFACE_MODEM);
    }

    return iface_modem_voice_type;
}
