/* WirePlumber * * Copyright (C) 2025 Richard Acayan and contributors * @author: Richard Acayan * * SPDX-License-Identifier: MIT */ #include #include #include "dbus-connection-state.h" #define WP_TYPE_MODEM_MANAGER wp_modem_manager_get_type () WP_DEFINE_LOCAL_LOG_TOPIC ("m-modem-manager") struct _WpModemManager { WpPlugin parent; WpPlugin *dbus; GDBusObjectManager *manager; GList *voice; GList *calls; guint n_calls; }; enum { SIGNAL_CALL_START, SIGNAL_CALL_STOP, N_SIGNALS, }; /* see ModemManager-enums.h */ enum MMCallState { MM_CALL_STATE_DIALING = 1, MM_CALL_STATE_RINGING_OUT = 2, MM_CALL_STATE_ACTIVE = 4, }; static guint signals[N_SIGNALS]; G_DECLARE_FINAL_TYPE (WpModemManager, wp_modem_manager, WP, MODEM_MANAGER, WpPlugin) G_DEFINE_TYPE (WpModemManager, wp_modem_manager, WP_TYPE_PLUGIN) static void wp_modem_manager_init (WpModemManager * self) { } static gboolean is_active_state (gint32 state) { return state == MM_CALL_STATE_DIALING || state == MM_CALL_STATE_RINGING_OUT || state == MM_CALL_STATE_ACTIVE; } static void active_calls_inc (WpModemManager * wpmm) { wpmm->n_calls++; if (wpmm->n_calls == 1) { wp_info_object (wpmm, "modem call started"); g_signal_emit (wpmm, signals[SIGNAL_CALL_START], 0); } } static void active_calls_dec (WpModemManager * wpmm) { wpmm->n_calls--; if (wpmm->n_calls == 0) { wp_info_object (wpmm, "modem call stopped"); g_signal_emit (wpmm, signals[SIGNAL_CALL_STOP], 0); } } static void on_call_state_change (GDBusProxy * iface, gchar * sender, gchar * signal, GVariant * params, gpointer data) { WpModemManager *wpmm = WP_MODEM_MANAGER (data); gint32 old, new; guint32 reason; if (g_strcmp0 (signal, "StateChanged")) return; g_variant_get (params, "(iiu)", &old, &new, &reason); if (!is_active_state (old) && is_active_state (new)) { active_calls_inc (wpmm); } else if (is_active_state (old) && !is_active_state (new)) { active_calls_dec (wpmm); } } static void bind_call (GObject * obj, GAsyncResult * res, gpointer data) { WpModemManager *wpmm = WP_MODEM_MANAGER (data); g_autoptr (GError) err = NULL; GDBusProxy *call; g_autoptr (GVariant) prop = NULL; gint init_state; call = g_dbus_proxy_new_finish (res, &err); if (call == NULL) { g_prefix_error (&err, "Failed to get call: "); wp_warning_object (wpmm, "%s", err->message); return; } prop = g_dbus_proxy_get_cached_property (call, "State"); if (prop == NULL) { wp_warning_object (wpmm, "Failed to get initial call state"); } else { g_variant_get (prop, "i", &init_state); if (is_active_state (init_state)) active_calls_inc (wpmm); } wpmm->calls = g_list_prepend (wpmm->calls, call); g_signal_connect (call, "g-signal", G_CALLBACK (on_call_state_change), wpmm); } static gint match_call_path (gconstpointer a, gconstpointer b) { GDBusProxy *call; const gchar *path, *call_path; if (G_IS_DBUS_PROXY (a)) { call = G_DBUS_PROXY (a); path = b; } else if (G_IS_DBUS_PROXY (b)) { call = G_DBUS_PROXY (b); path = a; } else { return 1; } call_path = g_dbus_proxy_get_object_path (call); return g_strcmp0 (path, call_path); } static void on_voice_signal (GDBusProxy * iface, gchar * sender, gchar * signal, GVariant * params, gpointer data) { WpModemManager *wpmm = WP_MODEM_MANAGER (data); GList *deleted; gchar *path; g_autoptr (GDBusConnection) conn = NULL; g_object_get (wpmm->dbus, "connection", &conn, NULL); if (!g_strcmp0 (signal, "CallAdded")) { g_variant_get (params, "(&o)", &path); g_dbus_proxy_new (conn, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.ModemManager1", path, "org.freedesktop.ModemManager1.Call", NULL, bind_call, wpmm); } else if (!g_strcmp0 (signal, "CallDeleted")) { g_variant_get (params, "(&o)", &path); // The user shouldn't have hundreds of calls, so just linear search. deleted = g_list_find_custom (wpmm->calls, path, match_call_path); if (deleted) { g_object_unref (deleted->data); wpmm->calls = g_list_delete_link (wpmm->calls, deleted); } } } static void list_calls_done (GObject * obj, GAsyncResult *res, gpointer data) { WpModemManager *wpmm = WP_MODEM_MANAGER (data); g_autoptr (GVariant) params = NULL; g_autoptr (GVariantIter) calls = NULL; gchar *path; g_autoptr (GError) err = NULL; g_autoptr (GDBusConnection) conn = NULL; params = g_dbus_proxy_call_finish (G_DBUS_PROXY (obj), res, &err); if (params == NULL) { g_prefix_error (&err, "Failed to list active calls on startup: "); wp_warning_object (wpmm, "%s", err->message); return; } g_object_get (wpmm->dbus, "connection", &conn, NULL); g_variant_get (params, "(ao)", &calls); while (g_variant_iter_loop (calls, "&o", &path)) { g_dbus_proxy_new (conn, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.ModemManager1", path, "org.freedesktop.ModemManager1.Call", NULL, bind_call, wpmm); } } static void hotplug_modem (GDBusObjectManager * mm, GDBusObject * obj, gpointer data) { WpModemManager *wpmm = WP_MODEM_MANAGER (data); GDBusInterface *iface; iface = g_dbus_object_get_interface (obj, "org.freedesktop.ModemManager1.Modem.Voice"); if (iface == NULL) return; g_dbus_proxy_call (G_DBUS_PROXY (iface), "ListCalls", g_variant_new ("()"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, list_calls_done, wpmm); wpmm->voice = g_list_prepend (wpmm->voice, iface); g_signal_connect (iface, "g-signal", G_CALLBACK (on_voice_signal), wpmm); } static void coldplug_modem (gpointer elt, gpointer data) { GDBusObject *obj = elt; WpModemManager *wpmm = data; GDBusInterface *iface; iface = g_dbus_object_get_interface (obj, "org.freedesktop.ModemManager1.Modem.Voice"); if (iface == NULL) return; g_dbus_proxy_call (G_DBUS_PROXY (iface), "ListCalls", g_variant_new ("()"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, list_calls_done, wpmm); wpmm->voice = g_list_prepend (wpmm->voice, iface); g_signal_connect (iface, "g-signal", G_CALLBACK (on_voice_signal), wpmm); } static void on_modemmanager_get (GObject * obj, GAsyncResult * res, gpointer data) { WpTransition *transition = WP_TRANSITION (data); GError *err = NULL; WpModemManager *wpmm; GList *modems; if (WP_IS_TRANSITION (data)) { // from wp_modem_manager_enable transition = WP_TRANSITION (data); wpmm = wp_transition_get_source_object (transition); } else { // from on_dbus_state_changed transition = NULL; wpmm = WP_MODEM_MANAGER (data); } wpmm->manager = g_dbus_object_manager_client_new_finish (res, &err); if (wpmm->manager == NULL) { g_prefix_error (&err, "Failed to connect to ModemManager: "); wp_warning_object (wpmm, "%s", err->message); if (transition) wp_transition_return_error (transition, g_steal_pointer (&err)); g_clear_object (&wpmm->dbus); return; } modems = g_dbus_object_manager_get_objects (wpmm->manager); g_list_foreach (modems, coldplug_modem, wpmm); g_list_free_full (g_steal_pointer (&modems), g_object_unref); g_signal_connect (wpmm->manager, "object-added", G_CALLBACK (hotplug_modem), wpmm); if (WP_IS_TRANSITION (data)) wp_object_update_features (WP_OBJECT (wpmm), WP_PLUGIN_FEATURE_ENABLED, 0); } static void on_dbus_state_changed (GObject * dbus, GParamSpec * spec, gpointer data) { WpDBusConnectionState state; WpModemManager *wpmm = WP_MODEM_MANAGER (data); g_autoptr (GDBusConnection) conn = NULL; g_object_get (wpmm->dbus, "state", &state, NULL); switch (state) { case WP_DBUS_CONNECTION_STATE_CONNECTED: g_object_get (wpmm->dbus, "connection", &conn, NULL); g_dbus_object_manager_client_new (conn, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, "org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1", NULL, NULL, NULL, NULL, on_modemmanager_get, wpmm); break; case WP_DBUS_CONNECTION_STATE_CONNECTING: case WP_DBUS_CONNECTION_STATE_CLOSED: g_list_free_full (g_steal_pointer (&wpmm->calls), g_object_unref); g_list_free_full (g_steal_pointer (&wpmm->voice), g_object_unref); g_clear_object (&wpmm->manager); break; default: g_assert_not_reached (); break; } } static void wp_modem_manager_enable (WpPlugin * self, WpTransition * transition) { WpModemManager *wpmm = WP_MODEM_MANAGER (self); g_autoptr (WpCore) core = NULL; GError *err; g_autoptr (GDBusConnection) conn = NULL; g_object_get (self, "core", &core, NULL); wpmm->dbus = wp_plugin_find (core, "system-dbus-connection"); if (!wpmm->dbus) { err = g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, "dbus-connection module must be loaded before modem-manager"); wp_transition_return_error (transition, err); return; } g_signal_connect_object (wpmm->dbus, "notify::state", G_CALLBACK (on_dbus_state_changed), wpmm, 0); g_object_get (wpmm->dbus, "connection", &conn, NULL); g_dbus_object_manager_client_new (conn, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, "org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1", NULL, NULL, NULL, NULL, on_modemmanager_get, transition); } static void wp_modem_manager_disable (WpPlugin * self) { WpModemManager *wpmm = WP_MODEM_MANAGER (self); g_list_free_full (g_steal_pointer (&wpmm->calls), g_object_unref); g_list_free_full (g_steal_pointer (&wpmm->voice), g_object_unref); g_clear_object (&wpmm->manager); g_clear_object (&wpmm->dbus); } static void wp_modem_manager_class_init (WpModemManagerClass * klass) { WpPluginClass *wpplugin_class = (WpPluginClass *) klass; wpplugin_class->enable = wp_modem_manager_enable; wpplugin_class->disable = wp_modem_manager_disable; signals[SIGNAL_CALL_START] = g_signal_new ("voice-call-start", WP_TYPE_MODEM_MANAGER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); signals[SIGNAL_CALL_STOP] = g_signal_new ("voice-call-stop", WP_TYPE_MODEM_MANAGER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } WP_PLUGIN_EXPORT GObject * wireplumber__module_init (WpCore * core, WpSpaJson * json, GError ** err) { return g_object_new (wp_modem_manager_get_type (), "name", "modem-manager", "core", core, NULL); }