/** * @file callstate.c * Call state module -- this handles the call state for MCE *

* Copyright (c) 2008 - 2009 Nokia Corporation and/or its subsidiary(-ies). * Copyright (c) 2012 - 2023 Jolla Ltd. *

* @author David Weinehall * @author Kalle Jokiniemi * @author Simo Piiroinen * * mce is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * mce 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mce. If not, see . */ #include "../mce.h" #include "../mce-log.h" #include "../mce-dbus.h" #include "../mce-wltimer.h" #include #include #include #include #if 0 // DEBUG: make all logging from this module "critical" # undef mce_log # define mce_log(LEV, FMT, ARGS...) \ mce_log_file(LL_CRIT, __FILE__, __FUNCTION__, FMT , ## ARGS) #endif /* ========================================================================= * * OFONO DBUS CONSTANTS * ========================================================================= */ #define OFONO_SERVICE "org.ofono" #define OFONO_MANAGER_INTERFACE "org.ofono.Manager" #define OFONO_MANAGER_OBJECT "/" #define OFONO_MANAGER_REQ_GET_MODEMS "GetModems" #define OFONO_MANAGER_SIG_MODEM_ADDED "ModemAdded" #define OFONO_MANAGER_SIG_MODEM_REMOVED "ModemRemoved" #define OFONO_MODEM_INTERFACE "org.ofono.Modem" #define OFONO_MODEM_SIG_PROPERTY_CHANGED "PropertyChanged" #define OFONO_VCALLMANAGER_INTERFACE "org.ofono.VoiceCallManager" #define OFONO_VCALLMANAGER_REQ_GET_CALLS "GetCalls" #define OFONO_VCALLMANAGER_SIG_CALL_ADDED "CallAdded" #define OFONO_VCALLMANAGER_SIG_CALL_REMOVED "CallRemoved" #define OFONO_VCALL_INTERFACE "org.ofono.VoiceCall" #define OFONO_VCALL_SIG_PROPERTY_CHANGED "PropertyChanged" /* ========================================================================= * * MODULE DETAILS * ========================================================================= */ /** Module name */ #define MODULE_NAME "callstate" /** Functionality provided by this module */ static const gchar *const provides[] = { MODULE_NAME, NULL }; /** Module information */ G_MODULE_EXPORT module_info_struct module_info = { /** Name of the module */ .name = MODULE_NAME, /** Module provides */ .provides = provides, /** Module priority */ .priority = 250 }; /* ========================================================================= * * MODULE DATA * ========================================================================= */ /** Maximum number of concurrent call state requesters */ #define CLIENTS_MONITOR_COUNT 15 /** List of monitored call state requesters */ static GSList *clients_monitor_list = NULL; /** Lookup table for state data / each call state requester */ static GHashTable *clients_state_lut = 0; static void call_state_rethink_schedule(void); static bool call_state_rethink_forced(void); static void xofono_get_vcalls(const char *modem); /* ========================================================================= * * OFONO CALL STATE HELPERS * ========================================================================= */ /** Enumeration of oFono voice call states */ typedef enum { OFONO_CALL_STATE_UNKNOWN, OFONO_CALL_STATE_ACTIVE, OFONO_CALL_STATE_HELD, OFONO_CALL_STATE_DIALING, OFONO_CALL_STATE_ALERTING, OFONO_CALL_STATE_INCOMING, OFONO_CALL_STATE_WAITING, OFONO_CALL_STATE_DISCONNECTED, OFONO_CALL_STATE_COUNT } ofono_callstate_t; /** Lookup table for oFono voice call states */ static const char * const ofono_callstate_lut[OFONO_CALL_STATE_COUNT] = { [OFONO_CALL_STATE_UNKNOWN] = "unknown", [OFONO_CALL_STATE_ACTIVE] = "active", [OFONO_CALL_STATE_HELD] = "held", [OFONO_CALL_STATE_DIALING] = "dialing", [OFONO_CALL_STATE_ALERTING] = "alerting", [OFONO_CALL_STATE_INCOMING] = "incoming", [OFONO_CALL_STATE_WAITING] = "waiting", [OFONO_CALL_STATE_DISCONNECTED] = "disconnected", }; /** oFono call state number to name */ #ifdef DEAD_CODE static const char * ofono_callstate_name(ofono_callstate_t callstate) { if( callstate < OFONO_CALL_STATE_COUNT ) return ofono_callstate_lut[callstate]; return ofono_callstate_lut[OFONO_CALL_STATE_UNKNOWN]; } #endif /** oFono call state name to number */ static ofono_callstate_t ofono_callstate_lookup(const char *name) { ofono_callstate_t res = OFONO_CALL_STATE_UNKNOWN; for( int i = 0; i < OFONO_CALL_STATE_COUNT; ++i ) { if( !strcmp(ofono_callstate_lut[i], name) ) { res = i; break; } } return res; } /** oFono call state name to mce call state number */ static call_state_t ofono_callstate_to_mce(const char *name) { ofono_callstate_t ofono = ofono_callstate_lookup(name); call_state_t mce = CALL_STATE_INVALID; switch( ofono ) { default: case OFONO_CALL_STATE_UNKNOWN: case OFONO_CALL_STATE_DISCONNECTED: mce = CALL_STATE_NONE; break; case OFONO_CALL_STATE_INCOMING: case OFONO_CALL_STATE_WAITING: mce = CALL_STATE_RINGING; break; case OFONO_CALL_STATE_DIALING: case OFONO_CALL_STATE_ALERTING: case OFONO_CALL_STATE_ACTIVE: case OFONO_CALL_STATE_HELD: mce = CALL_STATE_ACTIVE; break; } return mce; } /** oFono emergency flag to mce call type number */ static call_type_t ofono_calltype_to_mce(bool emergency) { return emergency ? CALL_TYPE_EMERGENCY : CALL_TYPE_NORMAL; } /* ========================================================================= * * OFONO VOICECALL OBJECTS * ========================================================================= */ /** oFono voice call state data */ typedef struct { char *name; bool probed; call_state_t state; call_type_t type; } ofono_vcall_t; static void clients_merge_state_cb(gpointer key, gpointer val, gpointer aptr); static void clients_merge_state (ofono_vcall_t *combined); static void clients_set_state (const char *dbus_name, const ofono_vcall_t *vcall); static void clients_get_state (const char *dbus_name, ofono_vcall_t *vcall); static void clients_init (void); static void clients_quit (void); /** Mark incoming vcall as ignored * * @param self oFono voice call object */ static void ofono_vcall_ignore_incoming_call(ofono_vcall_t *self) { if( self->state == CALL_STATE_RINGING ) { mce_log(LL_DEBUG, "ignoring incoming vcall: %s", self->name ?: "unnamed"); self->state = CALL_STATE_IGNORED; } } /** Merge emergency data to oFono voice call object * * @param self oFono voice call object * @param emergency emergency state to merge */ static void ofono_vcall_merge_emergency(ofono_vcall_t *self, bool emergency) { if( emergency ) self->type = CALL_TYPE_EMERGENCY; } /** Merge state data from oFono voice call object to another * * @param self oFono voice call object * @param emergency emergency state to merge */ static void ofono_vcall_merge_vcall(ofono_vcall_t *self, const ofono_vcall_t *that) { /* When evaluating combined call state, we must * give "ringing" state priority over "active" * so that display and suspend policy works in * expected manner. */ switch( that->state ) { case CALL_STATE_ACTIVE: if( self->state != CALL_STATE_RINGING ) self->state = CALL_STATE_ACTIVE; break; case CALL_STATE_RINGING: self->state = CALL_STATE_RINGING; break; default: break; } /* if any call is emergency, we have emergency call */ if( that->type == CALL_TYPE_EMERGENCY ) self->type = CALL_TYPE_EMERGENCY; } /** Create oFono voice call object * * @param name D-Bus object path * * @return oFono voice call object */ static ofono_vcall_t * ofono_vcall_create(const char *path) { ofono_vcall_t *self = calloc(1, sizeof *self); self->name = g_strdup(path); self->probed = false; self->state = CALL_STATE_INVALID; self->type = CALL_TYPE_NORMAL; mce_log(LL_DEBUG, "vcall=%s", self->name); return self; } /** Delete oFono voice call object * * @param self oFono voice call object */ static void ofono_vcall_delete(ofono_vcall_t *self) { if( self ) { mce_log(LL_DEBUG, "vcall=%s", self->name); g_free(self->name); free(self); } } /** Type agnostic callback function for deleting oFono voice call objects * * @param self oFono voice call object */ static void ofono_vcall_delete_cb(gpointer self) { ofono_vcall_delete(self); } /** Update oFono voice call object from key string and variant * * @param self oFono voice call object */ static void ofono_vcall_update_1(ofono_vcall_t *self, DBusMessageIter *iter) { const char *key = 0; DBusMessageIter var; if( !mce_dbus_iter_get_string(iter, &key) ) goto EXIT; if( !mce_dbus_iter_get_variant(iter, &var) ) goto EXIT; if( !strcmp(key, "Emergency") ) { bool emergency = false; if( !mce_dbus_iter_get_bool(&var, &emergency) ) goto EXIT; self->type = ofono_calltype_to_mce(emergency); mce_log(LL_DEBUG, "* %s = ofono:%s -> mce:%s", key, emergency ? "true" : "false", call_type_repr(self->type)); } else if( !strcmp(key, "State") ) { const char *str = 0; if( !mce_dbus_iter_get_string(&var, &str) ) goto EXIT; self->state = ofono_callstate_to_mce(str); mce_log(LL_DEBUG, "* %s = ofono:%s -> mce:%s", key,str, call_state_repr(self->state)); } #if 0 else { mce_log(LL_DEBUG, "* %s = %s", key, "..."); } #endif EXIT: return; } /** Update oFono voice call object from array of dict entries * * @param self oFono voice call object */ static void ofono_vcall_update_N(ofono_vcall_t *self, DBusMessageIter *iter) { DBusMessageIter arr2, dict; // self->probed = true; if( !mce_dbus_iter_get_array(iter, &arr2) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr2) ) { if( !mce_dbus_iter_get_entry(&arr2, &dict) ) goto EXIT; ofono_vcall_update_1(self, &dict); } EXIT: return; } /* ========================================================================= * * MANAGE VOICE CALL OBJECTS * ========================================================================= */ /** Lookup table for tracked voice call objects */ static GHashTable *vcalls_lut = 0; /** Lookup a voice call objects by name * * @param name D-Bus object path * * @return object pointer, or NULL if not found */ static ofono_vcall_t * vcalls_get_call(const char *name) { ofono_vcall_t *self = 0; if( !vcalls_lut ) goto EXIT; self = g_hash_table_lookup(vcalls_lut, name); EXIT: return self; } /** Lookup or create a voice call objects by name * * @param name D-Bus object path * * @return object pointer, or NULL on errors */ static ofono_vcall_t * vcalls_add_call(const char *name) { ofono_vcall_t *self = 0; if( !vcalls_lut ) goto EXIT; if( !(self = g_hash_table_lookup(vcalls_lut, name)) ) { self = ofono_vcall_create(name); g_hash_table_replace(vcalls_lut, g_strdup(name), self); } EXIT: return self; } /** Remove a voice call objects by name * * @param name D-Bus object path */ static void vcalls_rem_call(const char *name) { if( !vcalls_lut ) goto EXIT; g_hash_table_remove(vcalls_lut, name); EXIT: return; } /** Remove all tracked voice call objects * * @param name D-Bus object path */ static void vcalls_rem_calls(void) { if( vcalls_lut ) g_hash_table_remove_all(vcalls_lut); } /** Initialize voice call object look up table */ static void vcalls_init(void) { if( !vcalls_lut ) vcalls_lut = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, ofono_vcall_delete_cb); } /** Delete voice call object look up table */ static void vcalls_quit(void) { if( vcalls_lut ) g_hash_table_unref(vcalls_lut), vcalls_lut = 0; } /* ========================================================================= * * MODEM OBJECTS * ========================================================================= */ /** oFono modem tracking data */ typedef struct { /** D-Bus object path for the modem */ char *name; /** Flag for: Properties for this modem have been processed */ bool probed; /** Flag for: The Emergency call property for the modem is set */ bool emergency; /** Flag for: org.ofono.VoiceCallManager inteface is available */ bool vcalls_iface; /** Flag for: async dbus query to get vcalls for this modem is made */ bool vcalls_probed; } ofono_modem_t; /** Create oFono modem tracking object * * @param name D-Bus object path * * @return object pointer */ static ofono_modem_t * ofono_modem_create(const char *path) { ofono_modem_t *self = calloc(1, sizeof *self); self->name = g_strdup(path); self->probed = false; self->emergency = false; self->vcalls_iface = false; self->vcalls_probed = false; mce_log(LL_DEBUG, "modem=%s", self->name); return self; } /** Delete oFono modem tracking object * * @param self object pointer, or NULL */ static void ofono_modem_delete(ofono_modem_t *self) { if( self ) { mce_log(LL_DEBUG, "modem=%s", self->name); g_free(self->name); free(self); } } /** Type agnostic delete callback function for oFono modem tracking objects * * @param self object pointer, or NULL */ static void ofono_modem_delete_cb(gpointer self) { ofono_modem_delete(self); } /** Update oFono modem tracking object from key + variant data * * @param self object pointer * @param iter dbus message iterator pointing to key string + variant data */ static void ofono_modem_update_1(ofono_modem_t *self, DBusMessageIter *iter) { const char *key = 0; DBusMessageIter var; if( !mce_dbus_iter_get_string(iter, &key) ) goto EXIT; if( !mce_dbus_iter_get_variant(iter, &var) ) goto EXIT; if( !strcmp(key, "Emergency") ) { if( !mce_dbus_iter_get_bool(&var, &self->emergency) ) goto EXIT; mce_log(LL_DEBUG, "* %s = %s", key, self->emergency ? "true" : "false"); } else if( !strcmp(key, "Interfaces") ) { DBusMessageIter arr; if( !mce_dbus_iter_get_array(&var, &arr) ) goto EXIT; bool vcalls_iface = false; while( !mce_dbus_iter_at_end(&arr) ) { const char *iface = 0; if( !mce_dbus_iter_get_string(&arr, &iface) ) goto EXIT; if( strcmp(iface, OFONO_VCALLMANAGER_INTERFACE) ) continue; vcalls_iface = true; break; } if( self->vcalls_iface != vcalls_iface ) { self->vcalls_iface = vcalls_iface; self->vcalls_probed = false; mce_log(LL_NOTICE, "%s interface %savailable", OFONO_VCALLMANAGER_INTERFACE, self->vcalls_iface ? "" : "not "); } } #if 0 else { mce_log(LL_DEBUG, "* %s = %s", key, "..."); } #endif EXIT: return; } /** Update oFono modem tracking object from array of dict entries * * @param self object pointer * @param iter dbus message iterator pointing to array of dict entries */ static void ofono_modem_update_N(ofono_modem_t *self, DBusMessageIter *iter) { DBusMessageIter arr2, dict; self->probed = true; // if( !mce_dbus_iter_get_array(iter, &arr2) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr2) ) { if( !mce_dbus_iter_get_entry(&arr2, &dict) ) goto EXIT; ofono_modem_update_1(self, &dict); } EXIT: return; } /** Get voice calls for a modem * * If org.ofono.VoiceCallManager D-Bus interface is available for use, * enumerate voice call objects for the modem to get initial properties * of the calls. * * @param self object pointer */ static void ofono_modem_get_vcalls(ofono_modem_t *self) { /* Interface available? */ if( !self->vcalls_iface ) goto EXIT; /* Already done? */ if( self->vcalls_probed ) goto EXIT; /* Mark as done */ self->vcalls_probed = true; /* Start async D-Bus query */ xofono_get_vcalls(self->name); EXIT: return; } /* ========================================================================= * * MODEMS * ========================================================================= */ /** Lookup table for tracked oFono modem objects */ static GHashTable *modems_lut = 0; /** Lookup modem object by name * * @param name D-Bus object path * * @return object pointer, or NULL */ static ofono_modem_t * modems_get_modem(const char *name) { ofono_modem_t *self = 0; if( !modems_lut ) goto EXIT; self = g_hash_table_lookup(modems_lut, name); EXIT: return self; } /** Lookup existing / insert new modem object by name * * @param name D-Bus object path * * @return object pointer, or NULL */ static ofono_modem_t * modems_add_modem(const char *name) { ofono_modem_t *self = 0; if( !modems_lut ) goto EXIT; if( !(self = g_hash_table_lookup(modems_lut, name)) ) { self = ofono_modem_create(name); g_hash_table_replace(modems_lut, g_strdup(name), self); } EXIT: return self; } /** Remove modem object by name * * @param name D-Bus object path */ static void modems_rem_modem(const char *name) { if( !modems_lut ) goto EXIT; g_hash_table_remove(modems_lut, name); EXIT: return; } /** Remove all tracked modem objects */ static void modems_rem_all_modems(void) { if( modems_lut ) g_hash_table_remove_all(modems_lut); } /** Initialize lookup table for oFono modem tracking objects */ static void modems_init(void) { if( !modems_lut ) modems_lut = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, ofono_modem_delete_cb); } /** Delete lookup table for oFono modem tracking objects */ static void modems_quit(void) { if( modems_lut ) g_hash_table_unref(modems_lut), modems_lut = 0; } /* ========================================================================= * * OFONO DBUS GLUE * ========================================================================= */ /** Handle reply to voice calls query * * @param pc pending call object * @param aptr (not used) */ static void xofono_get_vcalls_cb(DBusPendingCall *pc, void *aptr) { (void) aptr; DBusMessage *rsp = 0; DBusError err = DBUS_ERROR_INIT; int cnt = 0; if( !(rsp = dbus_pending_call_steal_reply(pc)) ) { mce_log(LL_ERR, "%s: no reply", OFONO_VCALLMANAGER_REQ_GET_CALLS); goto EXIT; } if( dbus_set_error_from_message(&err, rsp) ) { mce_log(LL_ERR, "%s: %s", err.name, err.message); goto EXIT; } // DBusMessageIter body, arr1, mod; dbus_message_iter_init(rsp, &body); if( !mce_dbus_iter_get_array(&body, &arr1) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr1) ) { const char *name = 0; if( !mce_dbus_iter_get_struct(&arr1, &mod) ) goto EXIT; if( !mce_dbus_iter_get_object(&mod, &name) ) goto EXIT; ofono_vcall_t *vcall = vcalls_add_call(name); if( !vcall ) continue; ofono_vcall_update_N(vcall, &mod); ++cnt; } call_state_rethink_schedule(); EXIT: mce_log(LL_DEBUG, "added %d calls", cnt); if( rsp ) dbus_message_unref(rsp); dbus_error_free(&err); return; } /** Get voice calls associated with a modem * * Populate voice call lookup table with the reply data * * @param modem D-Bus object path */ static void xofono_get_vcalls(const char *modem) { dbus_send_ex(OFONO_SERVICE, modem, OFONO_VCALLMANAGER_INTERFACE, OFONO_VCALLMANAGER_REQ_GET_CALLS, xofono_get_vcalls_cb, 0,0,0, DBUS_TYPE_INVALID); } /** Handle voice call changed signal * * Update voice call lookup table with the content * * @param msg property changed signal */ static gboolean xofono_vcall_changed_cb(DBusMessage *const msg) { (void)msg; DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = dbus_message_get_path(msg); if( !name ) goto EXIT; ofono_vcall_t *vcall = vcalls_get_call(name); if( !vcall ) goto EXIT; ofono_vcall_update_1(vcall, &body); call_state_rethink_schedule(); EXIT: return TRUE; } /** Handle voice call added signal * * Update voice call lookup table with the content * * @param msg call added signal */ static gboolean xofono_vcall_added_cb(DBusMessage *const msg) { (void)msg; DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = 0; if( !mce_dbus_iter_get_object(&body, &name) ) goto EXIT; ofono_vcall_t *vcall = vcalls_add_call(name); if( vcall ) ofono_vcall_update_N(vcall, &body); call_state_rethink_schedule(); EXIT: return TRUE; } /** Handle voice call removed signal * * Update voice call lookup table with the content * * @param msg call removed signal */ static gboolean xofono_vcall_removed_cb(DBusMessage *const msg) { (void)msg; DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = 0; if( !mce_dbus_iter_get_object(&body, &name) ) goto EXIT; vcalls_rem_call(name); call_state_rethink_schedule(); EXIT: return TRUE; } /** Handle reply to xofono_get_modems() * * @param pc pending call object * @param aptr (not used) */ static void xofono_get_modems_cb(DBusPendingCall *pc, void *aptr) { (void)aptr; mce_log(LL_DEBUG, "%s.%s %s", OFONO_MANAGER_INTERFACE, OFONO_MANAGER_REQ_GET_MODEMS, "reply"); DBusMessage *rsp = 0; DBusError err = DBUS_ERROR_INIT; int cnt = 0; if( !(rsp = dbus_pending_call_steal_reply(pc)) ) goto EXIT; if( dbus_set_error_from_message(&err, rsp) ) { mce_log(LL_ERR, "%s: %s", err.name, err.message); goto EXIT; } // DBusMessageIter body, arr1, mod; dbus_message_iter_init(rsp, &body); if( !mce_dbus_iter_get_array(&body, &arr1) ) goto EXIT; while( !mce_dbus_iter_at_end(&arr1) ) { const char *name = 0; if( !mce_dbus_iter_get_struct(&arr1, &mod) ) goto EXIT; if( !mce_dbus_iter_get_object(&mod, &name) ) goto EXIT; ofono_modem_t *modem = modems_add_modem(name); if( !modem ) continue; ofono_modem_update_N(modem, &mod); ofono_modem_get_vcalls(modem); ++cnt; } call_state_rethink_schedule(); EXIT: mce_log(LL_DEBUG, "added %d modems", cnt); if( rsp ) dbus_message_unref(rsp); dbus_error_free(&err); } /** Get list of modems [async] * * Populate modem lookup table with the reply data */ static void xofono_get_modems(void) { bool res = dbus_send(OFONO_SERVICE, OFONO_MANAGER_OBJECT, OFONO_MANAGER_INTERFACE, OFONO_MANAGER_REQ_GET_MODEMS, xofono_get_modems_cb, DBUS_TYPE_INVALID); mce_log(LL_DEBUG, "%s.%s %s", OFONO_MANAGER_INTERFACE, OFONO_MANAGER_REQ_GET_MODEMS, res ? "sent ..." : "failed"); } /** Handle modem changed signal * * Update modem lookup table with the content * * @param msg property changed signal */ static gboolean xofono_modem_changed_cb(DBusMessage *const msg) { DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = dbus_message_get_path(msg); if( !name ) goto EXIT; mce_log(LL_NOTICE, "modem=%s", name); ofono_modem_t *modem = modems_get_modem(name); if( modem ) { ofono_modem_update_1(modem, &body); ofono_modem_get_vcalls(modem); } call_state_rethink_schedule(); EXIT: return TRUE; } /** Handle modem added signal * * Update modem lookup table with the content * * @param msg modem added signal */ static gboolean xofono_modem_added_cb(DBusMessage *const msg) { DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = 0; if( !mce_dbus_iter_get_object(&body, &name) ) goto EXIT; mce_log(LL_NOTICE, "modem=%s", name); ofono_modem_t *modem = modems_add_modem(name); if( modem ) { ofono_modem_update_N(modem, &body); ofono_modem_get_vcalls(modem); } call_state_rethink_schedule(); EXIT: return TRUE; } /** Handle modem removed signal * * Update modem lookup table with the content * * @param msg modem removed signal */ static gboolean xofono_modem_removed_cb(DBusMessage *const msg) { DBusMessageIter body; dbus_message_iter_init(msg, &body); const char *name = 0; if( !mce_dbus_iter_get_object(&body, &name) ) goto EXIT; mce_log(LL_NOTICE, "modem=%s", name); modems_rem_modem(name); call_state_rethink_schedule(); EXIT: return TRUE; } /* ========================================================================= * * OFONO TRACKING * ========================================================================= */ /** Flag for "org.ofono" D-Bus name has owner */ static bool xofono_is_available = false; /** Handle "org.ofono" D-Bus name owner changes * * Flush tracked modems and voice calls when name owner changes. * * Re-enumerate modems and calls when there is a new owner. * * @param available true if ofono name has an owner, false if not */ static void xofono_availability_set(bool available) { if( xofono_is_available != available ) { mce_log(LL_DEBUG, "%s is %savailable", OFONO_SERVICE, available ? "" : "not "); vcalls_rem_calls(); modems_rem_all_modems(); call_state_rethink_schedule(); if( (xofono_is_available = available) ) { /* start enumerating modems (async) */ xofono_get_modems(); } } } /** Handle D-Bus name owner changed signals for "org.ofono" */ static gboolean xofono_name_owner_changed_cb(DBusMessage *rsp) { const gchar *name = 0; const gchar *prev = 0; const gchar *curr = 0; DBusError err = DBUS_ERROR_INIT; if( dbus_set_error_from_message(&err, rsp) ) { mce_log(LL_ERR, "%s: %s", err.name, err.message); goto EXIT; } if( !dbus_message_get_args(rsp, &err, DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &prev, DBUS_TYPE_STRING, &curr, DBUS_TYPE_INVALID) ) { mce_log(LL_ERR, "%s: %s", err.name, err.message); goto EXIT; } if( !name || strcmp(name, OFONO_SERVICE) ) goto EXIT; xofono_availability_set(curr && *curr != 0 ); EXIT: dbus_error_free(&err); return TRUE; } /** Handle reply to asynchronous ofono service name ownership query * * @param pc State data for asynchronous D-Bus method call * @param user_data (not used) */ static void xofono_name_owner_get_cb(DBusPendingCall *pc, void *user_data) { (void)user_data; DBusMessage *rsp = 0; const char *owner = 0; DBusError err = DBUS_ERROR_INIT; if( !(rsp = dbus_pending_call_steal_reply(pc)) ) goto EXIT; if( dbus_set_error_from_message(&err, rsp) || !dbus_message_get_args(rsp, &err, DBUS_TYPE_STRING, &owner, DBUS_TYPE_INVALID) ) { if( strcmp(err.name, DBUS_ERROR_NAME_HAS_NO_OWNER) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); goto EXIT; } } xofono_availability_set(owner && *owner); EXIT: if( rsp ) dbus_message_unref(rsp); dbus_error_free(&err); } /** Initiate asynchronous ofono service name ownership query * * @return TRUE if the method call was initiated, or FALSE in case of errors */ static gboolean xofono_name_owner_get(void) { gboolean res = FALSE; const char *name = OFONO_SERVICE; res = dbus_send(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, "GetNameOwner", xofono_name_owner_get_cb, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID); return res; } /* ========================================================================= * * SIMULATED CALL STATE (for debugging purposes) * ========================================================================= */ /** Dummy vcall data used for clients that are not tracked */ static const ofono_vcall_t clients_vcall_def = { .state = CALL_STATE_NONE, .type = CALL_TYPE_NORMAL, }; /** Enumeration callback for ignoring incoming calls * * @param key D-Bus name of the client (as void pointer) * @param val ofono_vcall_t data of the client (as void pointer) * @param aptr NULL (unused) */ static void clients_ignore_incoming_calls_cb(gpointer key, gpointer val, gpointer aptr) { (void)key; (void)aptr; ofono_vcall_t *simulated = val; ofono_vcall_ignore_incoming_call(simulated); } /** Mark all incoming calls as ignored * * @param combined ofono_vcall_t data to update */ static void clients_ignore_incoming_calls(void) { if( !clients_state_lut ) goto EXIT; g_hash_table_foreach(clients_state_lut, clients_ignore_incoming_calls_cb, 0); EXIT: return; } /** Enumeration callback for evaluating combined dbus client state * * @param key D-Bus name of the client (as void pointer) * @param val ofono_vcall_t data of the client (as void pointer) * @param aptr ofono_vcall_t data to update (as void pointer) */ static void clients_merge_state_cb(gpointer key, gpointer val, gpointer aptr) { (void)key; ofono_vcall_t *combined = aptr; ofono_vcall_t *simulated = val; ofono_vcall_merge_vcall(combined, simulated); } /** Update overall call state by inspecting all active dbus client states * * @param combined ofono_vcall_t data to update */ static void clients_merge_state(ofono_vcall_t *combined) { if( !clients_state_lut ) goto EXIT; g_hash_table_foreach(clients_state_lut, clients_merge_state_cb, combined); EXIT: return; } /** Set state of one dbus client * * @ dbus_name D-Bus name of the client * @ vcall call state for the client, or NULL to remove client data */ static void clients_set_state(const char *dbus_name, const ofono_vcall_t *vcall) { if( !clients_state_lut || !dbus_name ) goto EXIT; if( vcall == 0 || vcall->state == CALL_STATE_NONE ) { g_hash_table_remove(clients_state_lut, dbus_name); goto EXIT; } ofono_vcall_t *cached = g_hash_table_lookup(clients_state_lut, dbus_name); if( !cached ) { cached = g_malloc0(sizeof *cached); g_hash_table_replace(clients_state_lut, g_strdup(dbus_name), cached); } *cached = *vcall; EXIT: return; } /** Get state of one dbus client * * Note: Untracked clients are assumed to be in none:normal call state. * * @ dbus_name D-Bus name of the client * @ vcall call state to fill in */ static void clients_get_state(const char *dbus_name, ofono_vcall_t *vcall) { ofono_vcall_t *cached = 0; if( clients_state_lut && dbus_name ) cached = g_hash_table_lookup(clients_state_lut, dbus_name); *vcall = cached ? *cached : clients_vcall_def; } /** Initialize dbus client tracking */ static void clients_init(void) { if( !clients_state_lut ) { clients_state_lut = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } } /** Stop dbus client tracking */ static void clients_quit(void) { /* Remove name owner monitors */ mce_dbus_owner_monitor_remove_all(&clients_monitor_list); /* Flush client state data */ if( clients_state_lut ) { g_hash_table_unref(clients_state_lut), clients_state_lut = 0; } } /** * Send the call state and type * * @param method_call A DBusMessage to reply to; * pass NULL to send a signal instead * @param call_state A string representation of an alternate state * to send instead of the real call state * @param call_type A string representation of an alternate type * to send instead of the real call type * @return TRUE on success, FALSE on failure */ static gboolean send_call_state(DBusMessage *const method_call, const gchar *const call_state, const gchar *const call_type) { DBusMessage *msg = NULL; gboolean status = FALSE; const gchar *sstate; const gchar *stype; /* Allow spoofing */ if (call_state != NULL) sstate = call_state; else sstate = call_state_to_dbus(datapipe_get_gint(call_state_pipe)); if (call_type != NULL) stype = call_type; else stype = call_type_repr(datapipe_get_gint(call_type_pipe)); /* If method_call is set, send a reply, * otherwise, send a signal */ if (method_call != NULL) { msg = dbus_new_method_reply(method_call); } else { /* sig_call_state_ind */ msg = dbus_new_signal(MCE_SIGNAL_PATH, MCE_SIGNAL_IF, MCE_CALL_STATE_SIG); mce_log(LL_DEVEL, "call state = %s / %s", sstate, stype); } /* Append the call state and call type */ if( !dbus_message_append_args(msg, DBUS_TYPE_STRING, &sstate, DBUS_TYPE_STRING, &stype, DBUS_TYPE_INVALID) ) { mce_log(LL_ERR, "Failed to append %sarguments to D-Bus message " "for %s.%s", method_call ? "reply " : "", method_call ? MCE_REQUEST_IF : MCE_SIGNAL_IF, method_call ? MCE_CALL_STATE_GET : MCE_CALL_STATE_SIG); goto EXIT; } /* Send the message if it is signal or wanted method reply */ if( !method_call || !dbus_message_get_no_reply(method_call) ) status = dbus_send_message(msg), msg = 0; EXIT: if( msg ) dbus_message_unref(msg); return status; } /** * D-Bus callback used for monitoring the process that requested * the call state; if that process exits, immediately * restore the call state to "none" and call type to "normal" * * @param msg The D-Bus message * @return TRUE on success, FALSE on failure */ static gboolean call_state_owner_monitor_dbus_cb(DBusMessage *const msg) { DBusError error = DBUS_ERROR_INIT; const char *dbus_name = 0; const char *old_owner = 0; const char *new_owner = 0; if( !dbus_message_get_args(msg, &error, DBUS_TYPE_STRING, &dbus_name, DBUS_TYPE_STRING, &old_owner, DBUS_TYPE_STRING, &new_owner, DBUS_TYPE_INVALID) ) { mce_log(LL_ERR, "Failed to parse NameOwnerChanged: %s: %s", error.name, error.message); goto EXIT; } /* Remove the name monitor for the call state requester */ if( mce_dbus_owner_monitor_remove(dbus_name, &clients_monitor_list) != -1 ) { clients_set_state(dbus_name, 0); call_state_rethink_schedule(); } EXIT: dbus_error_free(&error); return TRUE; } /** * D-Bus callback for the call state change request method call * * @param msg The D-Bus message * @return TRUE on success, FALSE on failure */ static gboolean change_call_state_dbus_cb(DBusMessage *const msg) { gboolean status = FALSE; const char *state = 0; const char *type = 0; const char *sender = dbus_message_get_sender(msg); DBusMessage *reply = NULL; DBusError error = DBUS_ERROR_INIT; dbus_bool_t changed = false; ofono_vcall_t prev = clients_vcall_def; ofono_vcall_t curr = clients_vcall_def; mce_log(LL_DEVEL, "Received set call state request from %s", mce_dbus_get_name_owner_ident(sender)); clients_get_state(sender, &prev); if( !dbus_message_get_args(msg, &error, DBUS_TYPE_STRING, &state, DBUS_TYPE_STRING, &type, DBUS_TYPE_INVALID) ) { mce_log(LL_ERR, "Failed to get argument from %s.%s: %s", MCE_REQUEST_IF, MCE_CALL_STATE_CHANGE_REQ, error.message); goto EXIT; } /* Convert call state to enum */ curr.state = call_state_from_dbus(state); if( curr.state == CALL_STATE_INVALID ) { mce_log(LL_WARN, "Invalid call state received; request ignored"); goto EXIT; } /* Convert call type to enum */ curr.type = call_type_parse(type); if( curr.type == CALL_TYPE_INVALID ) { mce_log(LL_WARN, "Invalid call type received; request ignored"); goto EXIT; } /* reject no-call emergency calls ... */ if( curr.state == CALL_STATE_NONE ) curr.type = CALL_TYPE_NORMAL; mce_log(LL_DEBUG, "Client call state changed: %s:%s -> %s:%s", call_state_repr(prev.state), call_type_repr(prev.type), call_state_repr(curr.state), call_type_repr(curr.type)); if( curr.state != CALL_STATE_NONE && mce_dbus_owner_monitor_add(sender, call_state_owner_monitor_dbus_cb, &clients_monitor_list, CLIENTS_MONITOR_COUNT) != -1 ) { clients_set_state(sender, &curr); } else { mce_dbus_owner_monitor_remove(sender, &clients_monitor_list); clients_set_state(sender, 0); } changed = call_state_rethink_forced(); EXIT: /* Setup the reply */ reply = dbus_new_method_reply(msg); /* Append the result */ if( !dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &changed, DBUS_TYPE_INVALID)) { mce_log(LL_ERR,"Failed to append reply arguments to D-Bus " "message for %s.%s", MCE_REQUEST_IF, MCE_CALL_STATE_CHANGE_REQ); } else if( !dbus_message_get_no_reply(msg) ) { status = dbus_send_message(reply), reply = 0; } if( reply ) dbus_message_unref(reply); dbus_error_free(&error); return status; } /** * D-Bus callback for the get call state method call * * @param msg The D-Bus message * @return TRUE on success, FALSE on failure */ static gboolean get_call_state_dbus_cb(DBusMessage *const msg) { gboolean status = FALSE; mce_log(LL_DEBUG, "Received call state get request"); /* Try to send a reply that contains the current call state and type */ if( !send_call_state(msg, NULL, NULL) ) goto EXIT; status = TRUE; EXIT: return status; } /* ========================================================================= * * MANAGE CALL STATE TRANSITIONS * ========================================================================= */ /** Callback for ignoring incoming voice call states */ static void call_state_ignore_incoming_calls_cb(gpointer key, gpointer value, gpointer aptr) { (void)key; //const char *name = key; (void)aptr; ofono_vcall_t *vcall = value; ofono_vcall_ignore_incoming_call(vcall); } /** Internally ignore incoming calls */ static void call_state_ignore_incoming_calls(void) { /* Consider simulated call states */ clients_ignore_incoming_calls(); /* consider ofono voice call properties */ if( vcalls_lut ) g_hash_table_foreach(vcalls_lut, call_state_ignore_incoming_calls_cb, 0); } /** Callback for merging voice call stats */ static void call_state_merge_vcall_cb(gpointer key, gpointer value, gpointer aptr) { (void)key; //const char *name = key; ofono_vcall_t *vcall = value; ofono_vcall_t *stats = aptr; ofono_vcall_merge_vcall(stats, vcall); } /** Callback for merging merging modem emergency data to voice call state */ static void call_state_merge_modem_cb(gpointer key, gpointer value, gpointer aptr) { (void)key; //const char *name = key; ofono_modem_t *modem = value; ofono_vcall_t *stats = aptr; ofono_vcall_merge_emergency(stats, modem->emergency); } /** Evaluate mce call state * * Emit signals and update data pipes as needed * * @return true if call state / type changed, false otherwise */ static bool call_state_rethink_now(void) { bool changed = false; static ofono_vcall_t previous = { .state = CALL_STATE_INVALID, .type = CALL_TYPE_NORMAL, }; ofono_vcall_t combined = { .state = CALL_STATE_NONE, .type = CALL_TYPE_NORMAL, }; /* consider simulated call state */ clients_merge_state(&combined); /* consider ofono modem emergency properties */ if( modems_lut ) g_hash_table_foreach(modems_lut, call_state_merge_modem_cb, &combined); /* consider ofono voice call properties */ if( vcalls_lut ) g_hash_table_foreach(vcalls_lut, call_state_merge_vcall_cb, &combined); /* skip broadcast if no change */ if( !memcmp(&previous, &combined, sizeof combined) ) goto EXIT; changed = true; previous = combined; call_state_t call_state = combined.state; call_type_t call_type = combined.type; const char *state_str = call_state_repr(call_state); const char *type_str = call_type_repr(call_type); mce_log(LL_DEBUG, "call_state=%s, call_type=%s", state_str, type_str); /* If the state changed, signal the new state; * first externally, then internally * * The reason we do it externally first is to * make sure that the camera application doesn't * grab audio, otherwise the ring tone might go missing */ // TODO: is the above legacy statement still valid? send_call_state(NULL, state_str, type_str); datapipe_exec_full(&call_state_pipe, GINT_TO_POINTER(call_state)); datapipe_exec_full(&call_type_pipe, GINT_TO_POINTER(call_type)); EXIT: return changed; } /** Idle timer for evaluating call state */ static mce_wltimer_t *call_state_rethink_tmr = 0; /** Timer callback for evaluating call state */ static gboolean call_state_rethink_cb(gpointer aptr) { (void)aptr; call_state_rethink_now(); return G_SOURCE_REMOVE; } /** Cancel delayed call state evaluation */ static void call_state_rethink_cancel(void) { mce_wltimer_stop(call_state_rethink_tmr); } /** Request delayed call state evaluation */ static void call_state_rethink_schedule(void) { mce_wltimer_start(call_state_rethink_tmr); } /** Request immediate call state evaluation */ static bool call_state_rethink_forced(void) { call_state_rethink_cancel(); return call_state_rethink_now(); } /* ========================================================================= * * D-BUS CALLBACKS * ========================================================================= */ /** Array of dbus message handlers */ static mce_dbus_handler_t callstate_dbus_handlers[] = { /* signals - outbound (for Introspect purposes only) */ { .interface = MCE_SIGNAL_IF, .name = MCE_CALL_STATE_SIG, .type = DBUS_MESSAGE_TYPE_SIGNAL, .args = " \n" " \n" }, /* method calls */ { .interface = MCE_REQUEST_IF, .name = MCE_CALL_STATE_CHANGE_REQ, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = change_call_state_dbus_cb, .args = " \n" " \n" " \n" }, { .interface = MCE_REQUEST_IF, .name = MCE_CALL_STATE_GET, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = get_call_state_dbus_cb, .args = " \n" " \n" }, /* signals */ { .interface = OFONO_MANAGER_INTERFACE, .name = OFONO_MANAGER_SIG_MODEM_ADDED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_modem_added_cb, }, { .interface = OFONO_MANAGER_INTERFACE, .name = OFONO_MANAGER_SIG_MODEM_REMOVED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_modem_removed_cb, }, { .interface = OFONO_MODEM_INTERFACE, .name = OFONO_MODEM_SIG_PROPERTY_CHANGED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_modem_changed_cb, }, { .interface = OFONO_VCALLMANAGER_INTERFACE, .name = OFONO_VCALLMANAGER_SIG_CALL_ADDED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_vcall_added_cb, }, { .interface = OFONO_VCALLMANAGER_INTERFACE, .name = OFONO_VCALLMANAGER_SIG_CALL_REMOVED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_vcall_removed_cb, }, { .interface = OFONO_VCALL_INTERFACE, .name = OFONO_VCALL_SIG_PROPERTY_CHANGED, .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_vcall_changed_cb, }, { .interface = DBUS_INTERFACE_DBUS, .name = "NameOwnerChanged", .rules = "arg0='"OFONO_SERVICE"'", .type = DBUS_MESSAGE_TYPE_SIGNAL, .callback = xofono_name_owner_changed_cb, }, /* sentinel */ { .interface = 0 } }; /** Add dbus handlers */ static void mce_callstate_init_dbus(void) { mce_dbus_handler_register_array(callstate_dbus_handlers); } /** Remove dbus handlers */ static void mce_callstate_quit_dbus(void) { mce_dbus_handler_unregister_array(callstate_dbus_handlers); } /* ========================================================================= * * DATAPIPE CALLBACKS * ========================================================================= */ /** Handle call state change notifications * * @param data call state (as void pointer) */ static void callstate_datapipe_ignore_incoming_call_event_cb(gconstpointer data) { bool ignore_incoming_call = GPOINTER_TO_INT(data); mce_log(LL_DEBUG, "ignore_incoming_call = %s", ignore_incoming_call ? "YES" : "NO"); // Note: Edge triggered if( !ignore_incoming_call ) goto EXIT; call_state_ignore_incoming_calls(); call_state_rethink_now(); EXIT: return; } /** Array of datapipe handlers */ static datapipe_handler_t callstate_datapipe_handlers[] = { // output triggers { .datapipe = &ignore_incoming_call_event_pipe, .output_cb = callstate_datapipe_ignore_incoming_call_event_cb, }, // sentinel { .datapipe = 0, } }; static datapipe_bindings_t callstate_datapipe_bindings = { .module = "callstate", .handlers = callstate_datapipe_handlers, }; /** Append triggers/filters to datapipes */ static void callstate_datapipes_init(void) { mce_datapipe_init_bindings(&callstate_datapipe_bindings); } /** Remove triggers/filters from datapipes */ static void callstate_datapipes_quit(void) { mce_datapipe_quit_bindings(&callstate_datapipe_bindings); } /* ========================================================================= * * MODULE LOAD / UNLOAD * ========================================================================= */ /** * Init function for the call state module * * @todo XXX status needs to be set on error! * * @param module Unused * @return NULL on success, a string with an error message on failure */ G_MODULE_EXPORT const gchar *g_module_check_init(GModule *module); const gchar *g_module_check_init(GModule *module) { (void)module; call_state_rethink_tmr = mce_wltimer_create("call_state_rethink", 0, call_state_rethink_cb, 0); /* create look up tables */ clients_init(); vcalls_init(); modems_init(); /* install datapipe hooks */ callstate_datapipes_init(); /* install dbus message handlers */ mce_callstate_init_dbus(); /* initiate async query to find out current state of ofono */ xofono_name_owner_get(); return NULL; } /** * Exit function for the call state module * * @todo D-Bus unregistration * * @param module Unused */ G_MODULE_EXPORT void g_module_unload(GModule *module); void g_module_unload(GModule *module) { (void)module; /* remove dbus message handlers */ mce_callstate_quit_dbus(); /* remove datapipe hooks */ callstate_datapipes_quit(); /* remove all timers & callbacks */ mce_wltimer_delete(call_state_rethink_tmr), call_state_rethink_tmr = 0; /* delete look up tables */ modems_quit(); vcalls_quit(); clients_quit(); return; }