imsd/implementations/ofonod.cpp
Jorijn van der Graaf b986eb3750
ofonod: GNOME Calls backend over org.ofono for Phosh and GNOME Mobile
imsd-ofonod presents imsd on the system bus as an oFono modem — Manager at
/, Modem + VoiceCallManager at /imsd, a VoiceCall object per call — which
is what GNOME Calls' bundled ofono provider drives; Calls, Phosh and GNOME
Shell run unmodified. org.gnome.Calls is not a seam a third party can
provide (Calls exports it, nothing feeds it), and Calls' provider plugins
link private headers, so org.ofono is the only D-Bus contract available.
Pure GDBus translation of net.catcrafts.IMS1, no core, like imsd-dialerd.

Packaged as the opt-in imsd-ofono subpackage: the daemon, its unit
(Conflicts=ofono.service), the system-bus policy and a gschema override
that points Calls at the ofono provider instead of mm (the ModemManager
origin would offer the modem's CS voice path, which carries no audio on
these phones). The policy grants root send_destination=org.ofono: under
dbus-broker a broadcast is checked against the sender's send policy for
every name the receiver owns, so without it imsd's call signals never
reach the daemon.

Verified on a Fairphone 6 with GNOME Calls 50.0: outgoing and incoming
calls with audio both ways, answered and hung up through
org.gnome.Calls.Call. Known Calls-side gap documented in the README: after
the VoiceCallManager interface is withdrawn and re-added, Calls keeps the
old origin's CallAdded handler and eventually crashes.
2026-09-20 16:07:46 +02:00

770 lines
33 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file fixed-width-types no-char-pointer
/*
imsd-ofonod — GNOME Calls backend for imsd (Phosh, GNOME Mobile).
Presents imsd as an oFono modem on the system bus: `org.ofono` with a Manager
at `/`, one Modem + VoiceCallManager at `/imsd`, and one VoiceCall object per
live call. GNOME Calls' bundled `ofono` provider plugin then drives imsd —
dial, ring, answer, hang up, call history, contact lookup — with Calls, Phosh
and GNOME Shell unmodified. It is the exact mirror of what imsd-dialerd does
for Plasma Mobile's org.kde.telephony.*.
Why this seam and not org.gnome.Calls: that interface is what Calls *exports*
so a shell can observe and control a call that already exists (Accept, Hangup,
SendDtmf, Silence — there is no Dial, and no way to register as a call
source). Phosh consumes it from Calls; nobody provides it to Calls. Calls'
own backends are libpeas provider plugins (mm, ofono, sip, dummy) linked
against private headers that no distro installs, so `org.ofono` is the only
D-Bus seam a third party can implement.
Consumer-shaped details, verified against calls 50.0
plugins/provider/ofono/ — get these wrong and the plugin silently ignores us:
- the provider watches `org.ofono` on the SYSTEM bus and proxies `/` as
org.ofono.Manager. A modem becomes a dialable Origin only while its
`Interfaces` property contains "org.ofono.VoiceCallManager", so that list
is our registration gate — the analogue of the dialerd's deviceUniList.
- VoiceCall `State` strings are parsed as CallsCallState enum nicks:
dialing / alerting / incoming / active / held / disconnected. imsd's
"ringing" (a 180 from the far end) is oFono's "alerting".
- inbound-ness is derived ONCE, from State == "incoming" as the call is
added, so an MT call must be announced incoming and never corrected later.
- DisconnectReason is cached by the plugin and read when CallRemoved
arrives — emit it before the removal or the history entry loses its
reason.
- Dial's returned object path is ignored (the plugin waits for CallAdded).
We return a real one anyway.
Runs as root because `org.ofono` is a system-bus name (policy in
packaging/org.ofono.conf). It does no I/O beyond D-Bus: every call decision
stays in imsd. Everything happens on the GLib main loop, so there is no
locking.
--session own org.ofono on the session bus and talk to imsd there
(development: no root, no policy file)
IMSD_BUS=session|system override only the imsd side of that choice
*/
#include <gio/gio.h>
#include <glib-unix.h>
import std;
namespace {
constexpr const char* ImsdBus = "net.catcrafts.IMS1";
constexpr const char* ImsdPath = "/net/catcrafts/IMS1";
constexpr const char* ImsdIface = "net.catcrafts.IMS1";
constexpr const char* OfonoBus = "org.ofono";
constexpr const char* ManagerPath = "/";
constexpr const char* ManagerIface = "org.ofono.Manager";
constexpr const char* ModemPath = "/imsd";
constexpr const char* ModemIface = "org.ofono.Modem";
constexpr const char* VcmIface = "org.ofono.VoiceCallManager";
constexpr const char* CallIface = "org.ofono.VoiceCall";
constexpr const char* SsIface = "org.ofono.SupplementaryServices";
constexpr const char* ErrFailed = "org.ofono.Error.Failed";
constexpr const char* ErrNotImplemented = "org.ofono.Error.NotImplemented";
constexpr const char* ErrNotSupported = "org.ofono.Error.NotSupported";
constexpr const char* ErrInvalidArgs = "org.ofono.Error.InvalidArguments";
void Log(std::string_view m) { std::println("imsd-ofonod: {}", m); std::fflush(stdout); }
std::string EnvOr(const char* k, std::string d) {
const char* v = std::getenv(k);
return v ? std::string(v) : std::move(d);
}
// ---- state (main thread only) ----------------------------------------------
GDBusConnection* Bus = nullptr; // where we own org.ofono
GDBusConnection* Imsd = nullptr; // where net.catcrafts.IMS1 lives
GMainLoop* Loop = nullptr;
bool ImsdUp = false;
bool Registered = false;
std::vector<std::string> EmergencyNumbers;
struct CallRec {
std::string uni, path, number;
std::string state = "dialing"; // oFono nick, already mapped
std::string reason; // oFono DisconnectReason, once known
bool inbound = false;
std::int64_t startedAt = 0;
guint regId = 0; // exported VoiceCall object
};
std::map<std::string, CallRec> Calls; // uni -> record
std::map<std::string, std::string> ByPath; // object path -> uni
std::map<std::string, std::string> UniToPath; // uni -> object path (survives
// a Dial reply that races the
// CallAdded signal)
unsigned PathSeq = 0;
// ---- GVariant helpers -------------------------------------------------------
std::string DictStr(GVariant* dict, const char* key) {
GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_STRING);
if (!v) return "";
std::string s = g_variant_get_string(v, nullptr);
g_variant_unref(v);
return s;
}
std::int64_t DictInt64(GVariant* dict, const char* key) {
GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_INT64);
if (!v) return 0;
std::int64_t x = g_variant_get_int64(v);
g_variant_unref(v);
return x;
}
bool DictBool(GVariant* dict, const char* key) {
GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_BOOLEAN);
if (!v) return false;
bool b = g_variant_get_boolean(v);
g_variant_unref(v);
return b;
}
// ---- imsd <-> oFono vocabulary ---------------------------------------------
// imsd call states -> oFono VoiceCall State nicks. Returns nullptr for a state
// we don't know, so the caller keeps the one it already published rather than
// inventing a transition.
const char* OfonoState(std::string_view s) {
if (s == "dialing") return "dialing";
if (s == "ringing") return "alerting"; // 180 from the far end
if (s == "incoming") return "incoming";
if (s == "active") return "active";
if (s == "terminated") return "disconnected";
return nullptr;
}
// imsd hangup reasons -> the three oFono DisconnectReason values.
const char* OfonoReason(std::string_view r) {
if (r == "local-hangup") return "local";
if (r == "remote-hangup" || r == "refused-or-busy") return "remote";
return "network";
}
// ---- signal emission --------------------------------------------------------
void Emit(const char* path, const char* iface, const char* name, GVariant* params) {
if (!Bus) return;
g_dbus_connection_emit_signal(Bus, nullptr, path, iface, name, params, nullptr);
}
GVariant* PropChanged(const char* name, GVariant* value) {
return g_variant_new("(sv)", name, value);
}
// ---- Modem / VoiceCallManager properties ------------------------------------
bool VoiceUp() { return ImsdUp && Registered; }
// The registration gate: the plugin only builds an Origin while
// org.ofono.VoiceCallManager is listed here.
GVariant* InterfacesValue() {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("as"));
if (VoiceUp()) g_variant_builder_add(&b, "s", VcmIface);
return g_variant_builder_end(&b);
}
GVariant* ModemProps() {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&b, "{sv}", "Name", g_variant_new_string("imsd"));
g_variant_builder_add(&b, "{sv}", "Manufacturer", g_variant_new_string("Catcrafts"));
g_variant_builder_add(&b, "{sv}", "Model", g_variant_new_string("userspace IMS"));
g_variant_builder_add(&b, "{sv}", "Type", g_variant_new_string("hardware"));
g_variant_builder_add(&b, "{sv}", "Powered", g_variant_new_boolean(TRUE));
g_variant_builder_add(&b, "{sv}", "Online", g_variant_new_boolean(VoiceUp() ? TRUE : FALSE));
g_variant_builder_add(&b, "{sv}", "Lockdown", g_variant_new_boolean(FALSE));
g_variant_builder_add(&b, "{sv}", "Interfaces", InterfacesValue());
return g_variant_builder_end(&b);
}
// EmergencyNumbers mirrors imsd's own configuration (builtin 112/911 plus the
// EMERGENCY_NUMBERS list) so an oFono client can display it. Classification
// itself is imsd's — it decides at Dial time whether a number becomes an
// urn:service:sos INVITE — and this list never gates a dial here.
GVariant* VcmProps() {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}"));
GVariantBuilder n;
g_variant_builder_init(&n, G_VARIANT_TYPE("as"));
for (const std::string& e : EmergencyNumbers)
g_variant_builder_add(&n, "s", e.c_str());
g_variant_builder_add(&b, "{sv}", "EmergencyNumbers", g_variant_builder_end(&n));
return g_variant_builder_end(&b);
}
// oFono's StartTime is an ISO 8601 string; imsd hands us epoch seconds.
std::string StartTimeOf(std::int64_t epoch) {
if (epoch <= 0) return "";
GDateTime* dt = g_date_time_new_from_unix_utc(epoch);
if (!dt) return "";
gchar* s = g_date_time_format_iso8601(dt);
std::string out = s ? s : "";
g_free(s);
g_date_time_unref(dt);
return out;
}
// Deliberately no "Emergency" key: imsd owns emergency classification and does
// not export its per-call verdict, and a second guess here could disagree with
// the INVITE that actually went out.
GVariant* CallProps(const CallRec& r) {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&b, "{sv}", "LineIdentification", g_variant_new_string(r.number.c_str()));
g_variant_builder_add(&b, "{sv}", "Name", g_variant_new_string(""));
g_variant_builder_add(&b, "{sv}", "State", g_variant_new_string(r.state.c_str()));
g_variant_builder_add(&b, "{sv}", "Multiparty", g_variant_new_boolean(FALSE));
g_variant_builder_add(&b, "{sv}", "RemoteHeld", g_variant_new_boolean(FALSE));
g_variant_builder_add(&b, "{sv}", "RemoteMultiparty", g_variant_new_boolean(FALSE));
std::string started = StartTimeOf(r.startedAt);
if (!started.empty())
g_variant_builder_add(&b, "{sv}", "StartTime", g_variant_new_string(started.c_str()));
return g_variant_builder_end(&b);
}
GVariant* CallVector() {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a(oa{sv})"));
for (const auto& [uni, rec] : Calls)
g_variant_builder_add(&b, "(o@a{sv})", rec.path.c_str(), CallProps(rec));
return g_variant_builder_end(&b);
}
// ---- imsd (client side) -----------------------------------------------------
// Fire-and-forget into imsd; a dead imsd shows up via the name watch, so a
// failure here is logged rather than fatal (same contract as imsd-dialerd).
void CallImsd(const char* method, GVariant* params) {
g_dbus_connection_call(
Imsd, ImsdBus, ImsdPath, ImsdIface, method, params, nullptr,
G_DBUS_CALL_FLAGS_NONE, 5000, nullptr,
[](GObject* src, GAsyncResult* res, gpointer m) {
GError* err = nullptr;
GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err);
if (err) {
Log(std::format("{} failed: {}", static_cast<const char*>(m), err->message));
g_error_free(err);
}
if (r) g_variant_unref(r);
},
const_cast<char*>(method));
}
std::string ActiveUni() {
for (const auto& [uni, rec] : Calls)
if (rec.state != "disconnected") return uni;
return "";
}
// ---- the VoiceCall objects --------------------------------------------------
constexpr const char* CallXml = R"xml(
<node>
<interface name="org.ofono.VoiceCall">
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
<method name="Deflect"><arg type="s" name="number" direction="in"/></method>
<method name="Hangup"/>
<method name="Answer"/>
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
<signal name="DisconnectReason"><arg type="s" name="reason"/></signal>
</interface>
</node>)xml";
void HandleCallMethod(GDBusConnection*, const gchar*, const gchar* objectPath, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) {
auto byPath = ByPath.find(objectPath ? objectPath : "");
if (byPath == ByPath.end()) {
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no such call");
return;
}
auto it = Calls.find(byPath->second);
if (it == Calls.end()) {
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no such call");
return;
}
std::string_view m = method;
if (m == "GetProperties") {
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", CallProps(it->second)));
return;
}
if (m == "Answer") {
Log(std::format("Answer({})", it->second.uni));
CallImsd("Accept", g_variant_new("(s)", it->second.uni.c_str()));
g_dbus_method_invocation_return_value(inv, nullptr);
return;
}
if (m == "Hangup") {
Log(std::format("Hangup({})", it->second.uni));
CallImsd("HangUp", g_variant_new("(s)", it->second.uni.c_str()));
g_dbus_method_invocation_return_value(inv, nullptr);
return;
}
if (m == "Deflect") {
g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "call deflection is not implemented");
return;
}
g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method");
}
const GDBusInterfaceVTable CallVtable = { HandleCallMethod, nullptr, nullptr, {} };
guint RegisterObject(const char* xml, const char* path, const GDBusInterfaceVTable* vtable) {
GDBusNodeInfo* node = g_dbus_node_info_new_for_xml(xml, nullptr);
guint id = g_dbus_connection_register_object(Bus, path, node->interfaces[0], vtable, nullptr, nullptr, nullptr);
g_dbus_node_info_unref(node);
return id;
}
// Allocate (or recall) the object path for an imsd call uni. imsd's unis
// ("ims-call-3") are not valid object-path elements, and a Dial reply can beat
// the CallAdded signal, so the mapping is created on first mention either way.
const std::string& PathFor(const std::string& uni) {
auto it = UniToPath.find(uni);
if (it != UniToPath.end()) return it->second;
std::string path = std::format("{}/voicecall{:02}", ModemPath, ++PathSeq);
ByPath[path] = uni;
return UniToPath.emplace(uni, std::move(path)).first->second;
}
void ForgetPath(const std::string& uni) {
auto it = UniToPath.find(uni);
if (it == UniToPath.end()) return;
ByPath.erase(it->second);
UniToPath.erase(it);
}
void AddCall(const std::string& uni, const std::string& number, std::string_view imsdState, std::int64_t startedAt) {
if (uni.empty() || Calls.contains(uni)) return;
const char* st = OfonoState(imsdState);
CallRec rec;
rec.uni = uni;
rec.path = PathFor(uni);
rec.number = number;
rec.state = st ? st : "dialing";
rec.inbound = rec.state == "incoming";
rec.startedAt = startedAt ? startedAt : g_get_real_time() / G_USEC_PER_SEC;
rec.regId = RegisterObject(CallXml, rec.path.c_str(), &CallVtable);
Log(std::format("call added {} {} {} [{}]", rec.path, rec.inbound ? "<-" : "->", rec.number, rec.state));
GVariant* props = CallProps(rec);
Calls[uni] = std::move(rec);
Emit(ModemPath, VcmIface, "CallAdded", g_variant_new("(o@a{sv})", Calls[uni].path.c_str(), props));
}
void SetCallState(CallRec& rec, const char* state) {
if (rec.state == state) return;
rec.state = state;
Emit(rec.path.c_str(), CallIface, "PropertyChanged",
PropChanged("State", g_variant_new_string(state)));
}
// The plugin caches DisconnectReason and reads it when CallRemoved lands, so
// the order here is load-bearing: disconnected state, then reason, then removal.
void RemoveCall(const std::string& uni, const char* reasonHint) {
auto it = Calls.find(uni);
if (it == Calls.end()) return;
CallRec& rec = it->second;
SetCallState(rec, "disconnected");
const char* reason = !rec.reason.empty() ? rec.reason.c_str()
: (reasonHint ? reasonHint : "network");
Emit(rec.path.c_str(), CallIface, "DisconnectReason", g_variant_new("(s)", reason));
if (rec.regId) g_dbus_connection_unregister_object(Bus, rec.regId);
std::string path = rec.path;
Log(std::format("call removed {} ({})", path, reason));
Calls.erase(it);
ForgetPath(uni);
Emit(ModemPath, VcmIface, "CallRemoved", g_variant_new("(o)", path.c_str()));
}
// ---- Manager / Modem / VoiceCallManager objects -----------------------------
constexpr const char* ManagerXml = R"xml(
<node>
<interface name="org.ofono.Manager">
<method name="GetModems"><arg type="a(oa{sv})" name="modems" direction="out"/></method>
<signal name="ModemAdded"><arg type="o" name="path"/><arg type="a{sv}" name="properties"/></signal>
<signal name="ModemRemoved"><arg type="o" name="path"/></signal>
</interface>
</node>)xml";
constexpr const char* ModemXml = R"xml(
<node>
<interface name="org.ofono.Modem">
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
<method name="SetProperty">
<arg type="s" name="property" direction="in"/>
<arg type="v" name="value" direction="in"/>
</method>
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
</interface>
</node>)xml";
constexpr const char* VcmXml = R"xml(
<node>
<interface name="org.ofono.VoiceCallManager">
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
<method name="Dial">
<arg type="s" name="number" direction="in"/>
<arg type="s" name="hide_callerid" direction="in"/>
<arg type="o" name="path" direction="out"/>
</method>
<method name="GetCalls"><arg type="a(oa{sv})" name="calls_with_properties" direction="out"/></method>
<method name="HangupAll"/>
<method name="SendTones"><arg type="s" name="tones" direction="in"/></method>
<method name="Transfer"/>
<method name="SwapCalls"/>
<method name="ReleaseAndAnswer"/>
<method name="ReleaseAndSwap"/>
<method name="HoldAndAnswer"/>
<method name="PrivateChat">
<arg type="o" name="call" direction="in"/>
<arg type="ao" name="calls" direction="out"/>
</method>
<method name="CreateMultiparty"><arg type="ao" name="calls" direction="out"/></method>
<method name="HangupMultiparty"/>
<method name="DialLast"/>
<method name="DialMemory"><arg type="u" name="memory_location" direction="in"/></method>
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
<signal name="CallAdded"><arg type="o" name="path"/><arg type="a{sv}" name="properties"/></signal>
<signal name="CallRemoved"><arg type="o" name="path"/></signal>
<signal name="Forwarded"><arg type="s" name="type"/></signal>
<signal name="BarringActive"><arg type="s" name="type"/></signal>
</interface>
</node>)xml";
// USSD has no path through the IMS stack. The interface is answered (Calls
// proxies it unconditionally, and a legible error beats UnknownInterface) but
// deliberately NOT advertised in the modem's Interfaces list.
constexpr const char* SsXml = R"xml(
<node>
<interface name="org.ofono.SupplementaryServices">
<method name="Initiate">
<arg type="s" name="command" direction="in"/>
<arg type="s" name="result_name" direction="out"/>
<arg type="v" name="value" direction="out"/>
</method>
<method name="Respond">
<arg type="s" name="reply" direction="in"/>
<arg type="s" name="result" direction="out"/>
</method>
<method name="Cancel"/>
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
<signal name="NotificationReceived"><arg type="s" name="message"/></signal>
<signal name="RequestReceived"><arg type="s" name="message"/></signal>
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
</interface>
</node>)xml";
void HandleManagerMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) {
if (std::string_view(method) == "GetModems") {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a(oa{sv})"));
g_variant_builder_add(&b, "(o@a{sv})", ModemPath, ModemProps());
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a(oa{sv}))", g_variant_builder_end(&b)));
return;
}
g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method");
}
void HandleModemMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant* params, GDBusMethodInvocation* inv, gpointer) {
std::string_view m = method;
if (m == "GetProperties") {
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", ModemProps()));
return;
}
if (m == "SetProperty") {
const gchar* prop = nullptr;
GVariant* value = nullptr;
g_variant_get(params, "(&sv)", &prop, &value);
if (value) g_variant_unref(value);
// Powered/Online track imsd's registration; nothing here is settable.
g_dbus_method_invocation_return_dbus_error(
inv, ErrNotSupported,
std::format("{} is driven by imsd's registration and cannot be set", prop ? prop : "?").c_str());
return;
}
g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method");
}
void OnDialReply(GObject* src, GAsyncResult* res, gpointer user) {
GDBusMethodInvocation* inv = static_cast<GDBusMethodInvocation*>(user);
GError* err = nullptr;
GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err);
if (!r) {
Log(std::format("imsd Dial failed: {}", err ? err->message : "?"));
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, err ? err->message : "imsd Dial failed");
if (err) g_error_free(err);
return;
}
const gchar* uni = nullptr;
g_variant_get(r, "(&s)", &uni);
if (!uni || !*uni) {
g_variant_unref(r);
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "imsd returned no call id");
return;
}
const std::string& path = PathFor(uni);
g_dbus_method_invocation_return_value(inv, g_variant_new("(o)", path.c_str()));
g_variant_unref(r);
}
void HandleVcmMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant* params, GDBusMethodInvocation* inv, gpointer) {
std::string_view m = method;
if (m == "GetProperties") {
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", VcmProps()));
return;
}
if (m == "GetCalls") {
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a(oa{sv}))", CallVector()));
return;
}
if (m == "Dial") {
const gchar* number = nullptr;
const gchar* clir = nullptr;
g_variant_get(params, "(&s&s)", &number, &clir);
if (!number || !*number) {
g_dbus_method_invocation_return_dbus_error(inv, ErrInvalidArgs, "empty number");
return;
}
// "default" is the only CLIR setting imsd can honour. Failing loudly on
// a withhold request beats dialling with the caller ID exposed.
std::string_view hide = clir ? clir : "";
if (!hide.empty() && hide != "default") {
g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "per-call caller-ID control is not implemented");
return;
}
if (!VoiceUp()) {
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "imsd is not registered");
return;
}
Log(std::format("Dial({})", number));
g_dbus_connection_call(Imsd, ImsdBus, ImsdPath, ImsdIface, "Dial",
g_variant_new("(s)", number), G_VARIANT_TYPE("(s)"),
G_DBUS_CALL_FLAGS_NONE, 10000, nullptr, OnDialReply, inv);
return;
}
if (m == "HangupAll") {
for (const auto& [uni, rec] : Calls)
if (rec.state != "disconnected")
CallImsd("HangUp", g_variant_new("(s)", uni.c_str()));
g_dbus_method_invocation_return_value(inv, nullptr);
return;
}
if (m == "SendTones") {
const gchar* tones = nullptr;
g_variant_get(params, "(&s)", &tones);
std::string target = ActiveUni();
if (target.empty()) {
g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no active call");
return;
}
CallImsd("SendDtmf", g_variant_new("(ss)", target.c_str(), tones ? tones : ""));
g_dbus_method_invocation_return_value(inv, nullptr);
return;
}
// Single-call stack: no hold, no multiparty, no transfer, no dial memory.
if (m == "Transfer" || m == "SwapCalls" || m == "ReleaseAndAnswer" || m == "ReleaseAndSwap"
|| m == "HoldAndAnswer" || m == "PrivateChat" || m == "CreateMultiparty"
|| m == "HangupMultiparty" || m == "DialLast" || m == "DialMemory") {
g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "not implemented by imsd");
return;
}
g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method");
}
void HandleSsMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) {
std::string_view m = method;
if (m == "GetProperties") {
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&b, "{sv}", "State", g_variant_new_string("idle"));
g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", g_variant_builder_end(&b)));
return;
}
if (m == "Cancel") {
g_dbus_method_invocation_return_value(inv, nullptr);
return;
}
g_dbus_method_invocation_return_dbus_error(inv, ErrNotSupported, "USSD is not supported on the IMS stack");
}
const GDBusInterfaceVTable ManagerVtable = { HandleManagerMethod, nullptr, nullptr, {} };
const GDBusInterfaceVTable ModemVtable = { HandleModemMethod, nullptr, nullptr, {} };
const GDBusInterfaceVTable VcmVtable = { HandleVcmMethod, nullptr, nullptr, {} };
const GDBusInterfaceVTable SsVtable = { HandleSsMethod, nullptr, nullptr, {} };
// ---- imsd events ------------------------------------------------------------
void PublishInterfaces() {
Emit(ModemPath, ModemIface, "PropertyChanged", PropChanged("Interfaces", InterfacesValue()));
Emit(ModemPath, ModemIface, "PropertyChanged",
PropChanged("Online", g_variant_new_boolean(VoiceUp() ? TRUE : FALSE)));
Log(std::format("voice interface {}", VoiceUp() ? "available" : "withdrawn"));
}
void OnImsdSignal(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* signal, GVariant* params, gpointer) {
std::string_view sig = signal;
if (sig == "CallAdded") {
const gchar* uni = nullptr;
GVariant* info = nullptr;
g_variant_get(params, "(&s@a{sv})", &uni, &info);
AddCall(uni ? uni : "", DictStr(info, "number"),
DictStr(info, "state").empty()
? (DictStr(info, "direction") == "incoming" ? "incoming" : "dialing")
: DictStr(info, "state"),
DictInt64(info, "startedAt"));
g_variant_unref(info);
} else if (sig == "CallStateChanged") {
const gchar* uni = nullptr;
const gchar* state = nullptr;
const gchar* reason = nullptr;
g_variant_get(params, "(&s&s&s)", &uni, &state, &reason);
auto it = Calls.find(uni ? uni : "");
if (it == Calls.end()) return;
const char* mapped = OfonoState(state ? state : "");
if (!mapped) return;
if (std::string_view(mapped) == "disconnected") {
// Bank the reason before the state flips: RemoveCall emits it.
it->second.reason = OfonoReason(reason ? reason : "");
SetCallState(it->second, mapped);
Log(std::format("{} -> disconnected ({})", it->second.path, it->second.reason));
return;
}
Log(std::format("{} -> {}", it->second.path, mapped));
SetCallState(it->second, mapped);
} else if (sig == "CallDeleted") {
const gchar* uni = nullptr;
g_variant_get(params, "(&s)", &uni);
RemoveCall(uni ? uni : "", "network");
} else if (sig == "RegistrationChanged") {
gboolean reg = FALSE;
g_variant_get(params, "(b)", &reg);
if (Registered == static_cast<bool>(reg)) return;
Registered = reg;
PublishInterfaces();
}
}
void RefreshFromImsd() {
GError* err = nullptr;
GVariant* st = g_dbus_connection_call_sync(Imsd, ImsdBus, ImsdPath, ImsdIface, "GetStatus", nullptr, G_VARIANT_TYPE("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, &err);
if (!st) {
Log(std::format("imsd status query failed: {}", err ? err->message : "?"));
if (err) g_error_free(err);
Registered = false;
return;
}
GVariant* dict = g_variant_get_child_value(st, 0);
Registered = DictBool(dict, "registered");
g_variant_unref(dict);
g_variant_unref(st);
GVariant* calls = g_dbus_connection_call_sync(Imsd, ImsdBus, ImsdPath, ImsdIface, "GetCalls", nullptr, G_VARIANT_TYPE("(aa{sv})"), G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, &err);
if (!calls) {
if (err) g_error_free(err);
return;
}
GVariant* arr = g_variant_get_child_value(calls, 0);
GVariantIter it;
g_variant_iter_init(&it, arr);
while (GVariant* info = g_variant_iter_next_value(&it)) {
AddCall(DictStr(info, "uni"), DictStr(info, "number"), DictStr(info, "state"), DictInt64(info, "startedAt"));
g_variant_unref(info);
}
g_variant_unref(arr);
g_variant_unref(calls);
}
void OnImsdAppeared(GDBusConnection*, const gchar*, const gchar*, gpointer) {
if (ImsdUp) return;
ImsdUp = true;
Log("imsd up");
RefreshFromImsd();
PublishInterfaces();
}
void OnImsdVanished(GDBusConnection*, const gchar*, gpointer) {
if (!ImsdUp) return;
ImsdUp = false;
Registered = false;
Log("imsd down");
std::vector<std::string> live;
for (const auto& [uni, rec] : Calls) live.push_back(uni);
for (const std::string& uni : live) RemoveCall(uni, "network");
PublishInterfaces();
}
gboolean OnTerm(gpointer loop) {
g_main_loop_quit(static_cast<GMainLoop*>(loop));
return G_SOURCE_REMOVE;
}
void LoadEmergencyNumbers() {
EmergencyNumbers = { "112", "911" };
std::string extra = EnvOr("EMERGENCY_NUMBERS", "");
for (std::size_t pos = 0; pos <= extra.size();) {
std::size_t comma = extra.find(',', pos);
std::size_t end = comma == std::string::npos ? extra.size() : comma;
std::string n = extra.substr(pos, end - pos);
std::erase(n, ' ');
if (!n.empty() && std::ranges::find(EmergencyNumbers, n) == EmergencyNumbers.end())
EmergencyNumbers.push_back(std::move(n));
if (comma == std::string::npos) break;
pos = comma + 1;
}
}
} // namespace
int main(int argc, char** argv) {
bool session = false;
for (int i = 1; i < argc; i++)
if (std::string_view(argv[i]) == "--session") session = true;
GBusType ofonoBus = session ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM;
std::string imsdBusName = EnvOr("IMSD_BUS", session ? "session" : "system");
GBusType imsdBus = imsdBusName == "session" ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM;
LoadEmergencyNumbers();
GError* err = nullptr;
Bus = g_bus_get_sync(ofonoBus, nullptr, &err);
if (!Bus) {
std::println(std::cerr, "imsd-ofonod: no {} bus: {}", session ? "session" : "system", err ? err->message : "?");
return 1;
}
Imsd = imsdBus == ofonoBus ? Bus : g_bus_get_sync(imsdBus, nullptr, &err);
if (!Imsd) {
std::println(std::cerr, "imsd-ofonod: no {} bus for imsd: {}", imsdBusName, err ? err->message : "?");
return 1;
}
RegisterObject(ManagerXml, ManagerPath, &ManagerVtable);
RegisterObject(ModemXml, ModemPath, &ModemVtable);
RegisterObject(VcmXml, ModemPath, &VcmVtable);
RegisterObject(SsXml, ModemPath, &SsVtable);
Loop = g_main_loop_new(nullptr, FALSE);
// DO_NOT_QUEUE: a real ofonod already owning the name means two stacks are
// fighting over the modem — fail loudly instead of waiting our turn.
guint owner = g_bus_own_name_on_connection(
Bus, OfonoBus, G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
[](GDBusConnection*, const gchar* n, gpointer) { Log(std::format("owning {}", n)); },
[](GDBusConnection*, const gchar* n, gpointer lp) {
Log(std::format("lost {} (is ofonod running?) — exiting", n));
g_main_loop_quit(static_cast<GMainLoop*>(lp));
},
Loop, nullptr);
g_dbus_connection_signal_subscribe(Imsd, ImsdBus, ImsdIface, nullptr, ImsdPath, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, OnImsdSignal, nullptr, nullptr);
g_bus_watch_name_on_connection(Imsd, ImsdBus, G_BUS_NAME_WATCHER_FLAGS_NONE, OnImsdAppeared, OnImsdVanished, nullptr, nullptr);
g_unix_signal_add(SIGTERM, OnTerm, Loop);
g_unix_signal_add(SIGINT, OnTerm, Loop);
Log(std::format("up (org.ofono on the {} bus, imsd on the {} bus)", session ? "session" : "system", imsdBusName));
g_main_loop_run(Loop);
g_bus_unown_name(owner);
g_main_loop_unref(Loop);
return 0;
}