/* -*- 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 * Copyright (C) 2019 Purism SPC */ #include #define _LIBMM_INSIDE_MM #include #include "mm-iface-modem.h" #include "mm-iface-modem-voice.h" #include "mm-call-list.h" #include "mm-error-helpers.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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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)) mm_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)) { mm_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) { mm_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); mm_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); mm_dbus_method_invocation_return_error_literal (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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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)) { mm_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)) { mm_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) { mm_dbus_method_invocation_return_error_literal (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; }