new file mode 100644
@@ -0,0 +1,713 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+
+#include <stdint.h>
+
+#include <glib.h>
+
+#include "lib/bluetooth.h"
+#include "bluetooth/sdp.h"
+#include "bluetooth/sdp_lib.h"
+#include "lib/uuid.h"
+
+#include "gdbus/gdbus.h"
+
+#include "btio/btio.h"
+#include "src/adapter.h"
+#include "src/btd.h"
+#include "src/dbus-common.h"
+#include "src/device.h"
+#include "src/error.h"
+#include "src/log.h"
+#include "src/plugin.h"
+#include "src/profile.h"
+#include "src/service.h"
+#include "src/shared/hfp.h"
+
+#include "telephony.h"
+
+#define TELEPHONY_AG_INTERFACE "org.bluez.TelephonyAg1"
+#define TELEPHONY_CALL_INTERFACE "org.bluez.TelephonyCall1"
+
+struct telephony {
+ struct btd_service *service;
+ struct btd_device *device;
+ char *path;
+ uint8_t id;
+ bdaddr_t src;
+ bdaddr_t dst;
+ void *profile_data;
+ struct telephony_callbacks *cbs;
+ enum connection_state state;
+ bool network_service;
+ uint8_t signal;
+ bool roaming;
+ uint8_t battchg;
+};
+
+static const char *state_to_string(enum connection_state state)
+{
+ switch (state) {
+ case CONNECTING:
+ return "connecting";
+ case SLC_CONNECTING:
+ return "slc_connecting";
+ case CONNECTED:
+ return "connected";
+ case DISCONNECTING:
+ return "disconnecting";
+ }
+
+ return NULL;
+}
+
+static const char *call_state_to_string(enum call_state state)
+{
+ switch (state) {
+ case CALL_STATE_ACTIVE:
+ return "active";
+ case CALL_STATE_HELD:
+ return "held";
+ case CALL_STATE_DIALING:
+ return "dialing";
+ case CALL_STATE_ALERTING:
+ return "alerting";
+ case CALL_STATE_INCOMING:
+ return "incoming";
+ case CALL_STATE_WAITING:
+ return "waiting";
+ case CALL_STATE_DISCONNECTED:
+ return "disconnected";
+ }
+
+ return NULL;
+}
+
+struct telephony *telephony_new(struct btd_service *service,
+ void *profile_data,
+ struct telephony_callbacks *cbs)
+{
+ struct btd_device *device = btd_service_get_device(service);
+ const char *path = device_get_path(device);
+ struct btd_adapter *adapter = device_get_adapter(device);
+ struct btd_profile *p = btd_service_get_profile(service);
+ struct telephony *ag;
+
+ ag = g_new0(struct telephony, 1);
+ bacpy(&ag->src, btd_adapter_get_address(adapter));
+ bacpy(&ag->dst, device_get_address(device));
+ ag->service = btd_service_ref(service);
+ ag->device = btd_device_ref(device);
+ ag->path = g_strdup_printf("%s/%s", path, p->name);
+ ag->profile_data = profile_data;
+ ag->cbs = cbs;
+
+ return ag;
+}
+
+void telephony_free(struct telephony *telephony)
+{
+ g_free(telephony->path);
+ g_free(telephony);
+}
+
+static DBusMessage *dial(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->dial)
+ return telephony->cbs->dial(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *swap_calls(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->swap_calls)
+ return telephony->cbs->swap_calls(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *release_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->release_and_answer)
+ return telephony->cbs->release_and_answer(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *release_and_swap(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->release_and_swap)
+ return telephony->cbs->release_and_swap(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *hold_and_answer(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->hold_and_answer)
+ return telephony->cbs->hold_and_answer(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *hangup_all(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->hangup_all)
+ return telephony->cbs->hangup_all(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *create_multiparty(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->create_multiparty)
+ return telephony->cbs->create_multiparty(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static DBusMessage *send_tones(DBusConnection *conn, DBusMessage *msg,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ if (telephony->cbs && telephony->cbs->send_tones)
+ return telephony->cbs->send_tones(conn, msg,
+ telephony->profile_data);
+
+ return btd_error_not_supported(msg);
+}
+
+static gboolean property_get_state(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ const char *string;
+
+ string = state_to_string(telephony->state);
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);
+
+ return TRUE;
+}
+
+static gboolean property_get_service(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ dbus_bool_t value;
+
+ value = telephony->network_service;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean property_get_signal(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+ &telephony->signal);
+
+ return TRUE;
+}
+
+static gboolean property_get_roaming(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+ dbus_bool_t value;
+
+ value = telephony->roaming;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean property_get_battchg(const GDBusPropertyTable *property,
+ DBusMessageIter *iter,
+ void *user_data)
+{
+ struct telephony *telephony = user_data;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE,
+ &telephony->battchg);
+
+ return TRUE;
+}
+
+static const GDBusMethodTable telephony_methods[] = {
+ { GDBUS_ASYNC_METHOD("Dial", GDBUS_ARGS({"number", "s"}), NULL,
+ dial) },
+ { GDBUS_ASYNC_METHOD("SwapCalls", NULL, NULL, swap_calls) },
+ { GDBUS_ASYNC_METHOD("ReleaseAndAnswer", NULL, NULL,
+ release_and_answer) },
+ { GDBUS_ASYNC_METHOD("ReleaseAndSwap", NULL, NULL,
+ release_and_swap) },
+ { GDBUS_ASYNC_METHOD("HoldAndAnswer", NULL, NULL,
+ hold_and_answer) },
+ { GDBUS_ASYNC_METHOD("HangupAll", NULL, NULL, hangup_all) },
+ { GDBUS_ASYNC_METHOD("CreateMultiparty", NULL,
+ GDBUS_ARGS({ "calls", "ao" }),
+ create_multiparty) },
+ { GDBUS_ASYNC_METHOD("SendTones", GDBUS_ARGS({"number", "s"}), NULL,
+ send_tones) },
+ { }
+};
+
+static const GDBusPropertyTable telephony_properties[] = {
+ { "State", "s", property_get_state },
+ { "Service", "b", property_get_service },
+ { "Signal", "y", property_get_signal },
+ { "Roaming", "b", property_get_roaming },
+ { "BattChg", "y", property_get_battchg },
+ { }
+};
+
+static void path_unregister(void *data)
+{
+ struct telephony *telephony = data;
+
+ DBG("Unregistered interface %s on path %s", TELEPHONY_AG_INTERFACE,
+ telephony->path);
+}
+
+int telephony_register_interface(struct telephony *telephony)
+{
+ if (telephony->cbs == NULL)
+ return -EINVAL;
+
+ if (!g_dbus_register_interface(btd_get_dbus_connection(),
+ telephony->path,
+ TELEPHONY_AG_INTERFACE,
+ telephony_methods, NULL,
+ telephony_properties, telephony,
+ path_unregister)) {
+ return -EINVAL;
+ }
+
+ DBG("Registered interface %s on path %s", TELEPHONY_AG_INTERFACE,
+ telephony->path);
+
+ return 0;
+}
+
+void telephony_unregister_interface(struct telephony *telephony)
+{
+ g_dbus_unregister_interface(btd_get_dbus_connection(), telephony->path,
+ TELEPHONY_AG_INTERFACE);
+}
+
+struct btd_service *telephony_get_service(struct telephony *telephony)
+{
+ return telephony->service;
+}
+
+struct btd_device *telephony_get_device(struct telephony *telephony)
+{
+ return telephony->device;
+}
+
+const char *telephony_get_path(struct telephony *telephony)
+{
+ return telephony->path;
+}
+
+bdaddr_t telephony_get_src(struct telephony *telephony)
+{
+ return telephony->src;
+}
+
+bdaddr_t telephony_get_dst(struct telephony *telephony)
+{
+ return telephony->dst;
+}
+
+void *telephony_get_profile_data(struct telephony *telephony)
+{
+ return telephony->profile_data;
+}
+
+void telephony_set_state(struct telephony *telephony,
+ enum connection_state state)
+{
+ char address[18];
+
+ if (telephony->state == state)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s state %s -> %s", address,
+ state_to_string(telephony->state),
+ state_to_string(state));
+
+ telephony->state = state;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "State");
+}
+
+enum connection_state telephony_get_state(struct telephony *telephony)
+{
+ return telephony->state;
+}
+
+void telephony_set_network_service(struct telephony *telephony, bool service)
+{
+ char address[18];
+
+ if (telephony->network_service == service)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s network service %u -> %u", address,
+ telephony->network_service,
+ service);
+
+ telephony->network_service = service;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Service");
+}
+
+bool telephony_get_network_service(struct telephony *telephony)
+{
+ return telephony->network_service;
+}
+
+void telephony_set_signal(struct telephony *telephony, uint8_t signal)
+{
+ char address[18];
+
+ if (telephony->signal == signal)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s signal %u -> %u", address, telephony->signal, signal);
+
+ telephony->signal = signal;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Signal");
+}
+
+uint8_t telephony_get_signal(struct telephony *telephony)
+{
+ return telephony->signal;
+}
+
+void telephony_set_roaming(struct telephony *telephony, bool roaming)
+{
+ char address[18];
+
+ if (telephony->roaming == roaming)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s roaming %u -> %u", address,
+ telephony->roaming,
+ roaming);
+
+ telephony->roaming = roaming;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "Roaming");
+}
+
+bool telephony_get_roaming(struct telephony *telephony)
+{
+ return telephony->roaming;
+}
+
+void telephony_set_battchg(struct telephony *telephony, uint8_t battchg)
+{
+ char address[18];
+
+ if (telephony->battchg == battchg)
+ return;
+
+ ba2str(&telephony->dst, address);
+ DBG("device %s battchg %u -> %u", address, telephony->battchg, battchg);
+
+ telephony->battchg = battchg;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ telephony->path, TELEPHONY_AG_INTERFACE,
+ "BattChg");
+}
+
+uint8_t telephony_get_battchg(struct telephony *telephony)
+{
+ return telephony->battchg;
+}
+
+struct call *telephony_new_call(struct telephony *telephony,
+ enum call_state state,
+ void *user_data)
+{
+ struct call *call;
+
+ call = g_new0(struct call, 1);
+ call->device = telephony;
+ call->state = state;
+ call->idx = telephony->id++;
+ call->path = g_strdup_printf("%s/call%u", telephony->path, call->idx);
+
+ return call;
+}
+
+void telephony_free_call(struct call *call)
+{
+ if (call->pending_msg)
+ dbus_message_unref(call->pending_msg);
+
+ g_free(call->name);
+ g_free(call->incoming_line);
+ g_free(call->line_id);
+ g_free(call->path);
+ g_free(call);
+}
+
+static DBusMessage *call_answer(DBusConnection *conn, DBusMessage *msg,
+ void *call_data)
+{
+ struct call *call = call_data;
+ struct telephony *telephony = call->device;
+
+ return telephony->cbs->call_answer(conn, msg, call_data);
+}
+
+static DBusMessage *call_hangup(DBusConnection *conn, DBusMessage *msg,
+ void *call_data)
+{
+ struct call *call = call_data;
+ struct telephony *telephony = call->device;
+
+ return telephony->cbs->call_hangup(conn, msg, call_data);
+}
+
+static gboolean call_line_id_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->line_id != NULL;
+}
+
+static gboolean call_property_get_line_id(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->line_id == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &call->line_id);
+
+ return TRUE;
+}
+
+static gboolean call_incoming_line_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->incoming_line != NULL;
+}
+
+static gboolean call_property_get_incoming_line(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->incoming_line == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
+ &call->incoming_line);
+
+ return TRUE;
+}
+
+static gboolean call_name_exists(const GDBusPropertyTable *property,
+ void *data)
+{
+ struct call *call = data;
+
+ return call->name != NULL;
+}
+
+static gboolean call_property_get_name(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+
+ if (call->name == NULL)
+ return FALSE;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &call->name);
+
+ return TRUE;
+}
+
+static gboolean call_property_get_multiparty(
+ const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+ dbus_bool_t value;
+
+ value = call->multiparty;
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
+
+ return TRUE;
+}
+
+static gboolean call_property_get_state(const GDBusPropertyTable *property,
+ DBusMessageIter *iter, void *data)
+{
+ struct call *call = data;
+ const char *string;
+
+ string = call_state_to_string(call->state);
+
+ dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string);
+
+ return TRUE;
+}
+
+static const GDBusMethodTable telephony_call_methods[] = {
+ { GDBUS_ASYNC_METHOD("Answer", NULL, NULL, call_answer) },
+ { GDBUS_ASYNC_METHOD("Hangup", NULL, NULL, call_hangup) },
+ { }
+};
+
+static const GDBusPropertyTable telephony_call_properties[] = {
+ { "LineIdentification", "s", call_property_get_line_id, NULL,
+ call_line_id_exists },
+ { "IncomingLine", "s", call_property_get_incoming_line, NULL,
+ call_incoming_line_exists },
+ { "Name", "s", call_property_get_name, NULL, call_name_exists },
+ { "Multiparty", "b", call_property_get_multiparty },
+ { "State", "s", call_property_get_state },
+ { }
+};
+
+static void call_path_unregister(void *user_data)
+{
+ struct call *call = user_data;
+
+ DBG("Unregistered interface %s on path %s", TELEPHONY_CALL_INTERFACE,
+ call->path);
+
+ telephony_free_call(call);
+}
+
+int telephony_call_register_interface(struct call *call)
+{
+ if (call->device->cbs == NULL)
+ return -EINVAL;
+
+ if (!g_dbus_register_interface(btd_get_dbus_connection(),
+ call->path,
+ TELEPHONY_CALL_INTERFACE,
+ telephony_call_methods, NULL,
+ telephony_call_properties, call,
+ call_path_unregister)) {
+ return -EINVAL;
+ }
+
+ DBG("Registered interface %s on path %s", TELEPHONY_CALL_INTERFACE,
+ call->path);
+
+ return 0;
+}
+
+void telephony_call_unregister_interface(struct call *call)
+{
+ g_dbus_unregister_interface(btd_get_dbus_connection(),
+ call->path,
+ TELEPHONY_CALL_INTERFACE);
+}
+
+void telephony_set_call_state(struct call *call, enum call_state state)
+{
+ if (call->state == state)
+ return;
+
+ DBG("%s state %s -> %s", call->path, call_state_to_string(call->state),
+ call_state_to_string(state));
+
+ call->state = state;
+
+ g_dbus_emit_property_changed(btd_get_dbus_connection(),
+ call->device->path, TELEPHONY_CALL_INTERFACE,
+ "State");
+}
+
+enum call_state telephony_get_call_state(struct call *call)
+{
+ return call->state;
+}
new file mode 100644
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright © 2025 Collabora Ltd.
+ *
+ *
+ */
+
+enum connection_state {
+ CONNECTING = 0,
+ SLC_CONNECTING,
+ CONNECTED,
+ DISCONNECTING
+};
+
+enum call_state {
+ CALL_STATE_ACTIVE = 0,
+ CALL_STATE_HELD,
+ CALL_STATE_DIALING,
+ CALL_STATE_ALERTING,
+ CALL_STATE_INCOMING,
+ CALL_STATE_WAITING,
+ CALL_STATE_DISCONNECTED,
+};
+
+struct telephony;
+
+struct telephony_callbacks {
+ DBusMessage *(*dial)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*swap_calls)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*release_and_answer)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*release_and_swap)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*hold_and_answer)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*hangup_all)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*create_multiparty)(DBusConnection *conn,
+ DBusMessage *msg,
+ void *profile_data);
+ DBusMessage *(*send_tones)(DBusConnection *conn, DBusMessage *msg,
+ void *profile_data);
+
+ DBusMessage *(*call_answer)(DBusConnection *conn, DBusMessage *msg,
+ void *call_data);
+ DBusMessage *(*call_hangup)(DBusConnection *conn, DBusMessage *msg,
+ void *call_data);
+ const char *(*call_get_line_id)(void *call_data);
+ const char *(*call_get_incoming_line)(void *call_data);
+ const char *(*call_get_name)(void *call_data);
+ bool (*call_get_multiparty)(void *call_data);
+ enum call_state (*call_get_state)(void *call_data);
+};
+
+struct call {
+ struct telephony *device;
+ char *path;
+ uint8_t idx;
+
+ char *line_id;
+ char *incoming_line;
+ char *name;
+ bool multiparty;
+ enum call_state state;
+
+ DBusMessage *pending_msg;
+};
+
+struct telephony *telephony_new(struct btd_service *service,
+ void *profile_data,
+ struct telephony_callbacks *cbs);
+void telephony_free(struct telephony *telephony);
+int telephony_register_interface(struct telephony *telephony);
+void telephony_unregister_interface(struct telephony *telephony);
+
+struct btd_service *telephony_get_service(struct telephony *telephony);
+struct btd_device *telephony_get_device(struct telephony *telephony);
+const char *telephony_get_path(struct telephony *telephony);
+bdaddr_t telephony_get_src(struct telephony *telephony);
+bdaddr_t telephony_get_dst(struct telephony *telephony);
+void *telephony_get_profile_data(struct telephony *telephony);
+void telephony_set_state(struct telephony *telephony,
+ enum connection_state state);
+enum connection_state telephony_get_state(struct telephony *telephony);
+void telephony_set_network_service(struct telephony *telephony, bool service);
+bool telephony_get_network_service(struct telephony *telephony);
+void telephony_set_signal(struct telephony *telephony, uint8_t signal);
+uint8_t telephony_get_signal(struct telephony *telephony);
+void telephony_set_roaming(struct telephony *telephony, bool roaming);
+bool telephony_get_roaming(struct telephony *telephony);
+void telephony_set_battchg(struct telephony *telephony, uint8_t battchg);
+uint8_t telephony_get_battchg(struct telephony *telephony);
+
+struct call *telephony_new_call(struct telephony *telephony,
+ enum call_state state,
+ void *user_data);
+void telephony_free_call(struct call *call);
+int telephony_call_register_interface(struct call *call);
+void telephony_call_unregister_interface(struct call *call);
+
+void telephony_set_call_state(struct call *call, enum call_state state);
+enum call_state telephony_get_call_state(struct call *call);