aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2021-12-20 17:17:37 -0500
committerScott Murray <scott.murray@konsulko.com>2021-12-20 17:55:18 -0500
commit701f44587eabaa78dc1c180d8ca0ac8f848941d5 (patch)
tree3c81191543cfa4be02f0b1237fd1129916b8e0ab
parent5a2ac9498d1aed3d1307dde3a3028dd3444bba0f (diff)
GLib based interface library for BlueZ, factored out of the agl-service-bluetooth binding code. See README.md for build and usage notes and the mapping of the new source files to those in the binding if that is for some reason required. Currently untested functionality: * autoconnect on startup * mediaplayer controls Bug-AGL: SPEC-4182 Signed-off-by: Scott Murray <scott.murray@konsulko.com> Change-Id: I7e70962ebabb138f81b9ba69af82f4ca0152dc31
-rw-r--r--.gitreview5
-rw-r--r--LICENSE54
-rw-r--r--README.md56
-rw-r--r--include/bluez-glib.h161
-rw-r--r--include/meson.build1
-rw-r--r--meson.build16
-rw-r--r--meson_options.txt1
-rw-r--r--src/api.c1543
-rw-r--r--src/bluez-agent.c313
-rw-r--r--src/bluez-agent.h40
-rw-r--r--src/bluez-call.c466
-rw-r--r--src/bluez-call.h227
-rw-r--r--src/call_work.c214
-rw-r--r--src/call_work.h65
-rw-r--r--src/common.h93
-rw-r--r--src/conf.c130
-rw-r--r--src/conf.h36
-rw-r--r--src/meson.build19
-rw-r--r--src/test.c232
19 files changed, 3672 insertions, 0 deletions
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..74f7dec
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.automotivelinux.org
+port=29418
+project=src/bluez-glib
+defaultbranch=master
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..31c692a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,54 @@
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+ You must give any other recipients of the Work or Derivative Works a copy of this License; and
+ You must cause any modified files to carry prominent notices stating that You changed the files; and
+ You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+ If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..38e3495
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+GLib BlueZ interface library
+----------------------------
+Derived from AGL Bluetooth binding source at
+[https://git.automotivelinux.org/apps/agl-service-bluetooth/]
+
+Source refactoring from the binding (not necessarily exhaustive):
+* bluetooth-agent.c -> bluez-agent.c
+* bluetooth-api.c -> api.c, call_work.c
+* bluetooth-bluez.c -> bluez-call.c
+* bluetooth-common.h -> common.h, bluez-call.h, call_work.h
+* bluetooth-conf.c -> conf.c
+* bluetooth-util.c -> mostly redundant, required things moved to api.c, bluez-call.c
+
+Copyright dates in files reflect their original creation dates and include
+revisions made in agl-service-bluetooth before December 2021.
+
+Building
+--------
+The build requirements are:
+* glib 2.0 headers and libraries (from e.g. glib2-devel on Fedora or CentOS,
+ libglib2.0-dev on Debian or Ubuntu).
+* meson
+
+To build:
+```
+meson build/
+ninja -C build/
+```
+
+Usage Notes
+-----------
+* Users only need include `bluez-glib.h` and link to the library.
+* API calls generally return a gboolean, with `FALSE` indicating failure.
+* `bluez_init` must be called before any other API calls except
+ `bluez_set_log_level` or one of the callback registration functions
+ (e.g. `bluez_add_adapter_event_callback`).
+* A return code of `TRUE` from `bluez_init` indicates D-Bus connection to
+ **BlueZ** has succeeded, but other API calls will not be useful until the
+ provided callback has been called indicating a success status. This
+ behavior stems from the possibility of running on systems without Bluetooth
+ adapters present.
+* Callbacks may be registered after calling `bluez_init`, but note that there
+ is a possibility that registration calls may briefly block if they occur
+ during processing of an associated event.
+* It is advised that only one primary user of the library enable agent and
+ autoconnect support to avoid conflicts.
+
+Contributing
+------------
+Questions can be sent to the agl-dev-community mailing list at
+<https://lists.automotivelinux.org/g/agl-dev-community>.
+
+Bugs can be filed on the AGL JIRA instance at <https://jira.automotivelinux.org>.
+
+Source contributions need to go through the AGL Gerrit instance, see
+<https://wiki.automotivelinux.org/agl-distro/contributing>.
diff --git a/include/bluez-glib.h b/include/bluez-glib.h
new file mode 100644
index 0000000..25abecb
--- /dev/null
+++ b/include/bluez-glib.h
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2021 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUEZ_GLIB_H
+#define BLUEZ_GLIB_H
+
+#include <glib.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+typedef enum {
+ BLUEZ_LOG_LEVEL_ERROR,
+ BLUEZ_LOG_LEVEL_WARNING,
+ BLUEZ_LOG_LEVEL_INFO,
+ BLUEZ_LOG_LEVEL_DEBUG
+} bluez_log_level_t;
+
+// Hook to allow users to override the default level
+#ifndef BLUEZ_LOG_LEVEL_DEFAULT
+#define BLUEZ_LOG_LEVEL_DEFAULT BLUEZ_LOG_LEVEL_ERROR
+#endif
+
+typedef enum {
+ BLUEZ_EVENT_ADD,
+ BLUEZ_EVENT_REMOVE,
+ BLUEZ_EVENT_CHANGE
+} bluez_event_t;
+
+typedef enum {
+ BLUEZ_AGENT_EVENT_REQUEST_CONFIRMATION,
+ BLUEZ_AGENT_EVENT_AUTHORIZE_SERVICE,
+ BLUEZ_AGENT_EVENT_CANCELLED_PAIRING
+} bluez_agent_event_t;
+
+typedef enum {
+ BLUEZ_MEDIA_CONTROL_CONNECT,
+ BLUEZ_MEDIA_CONTROL_DISCONNECT,
+ BLUEZ_MEDIA_CONTROL_PLAY,
+ BLUEZ_MEDIA_CONTROL_PAUSE,
+ BLUEZ_MEDIA_CONTROL_STOP,
+ BLUEZ_MEDIA_CONTROL_NEXT,
+ BLUEZ_MEDIA_CONTROL_PREVIOUS,
+ BLUEZ_MEDIA_CONTROL_FASTFORWARD,
+ BLUEZ_MEDIA_CONTROL_REWIND
+} bluez_media_control_t;
+
+typedef void (*bluez_init_cb_t)(gchar *adapter, gboolean status, gpointer user_data);
+
+typedef void (*bluez_adapter_event_cb_t)(gchar *adapter,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data);
+
+typedef void (*bluez_device_event_cb_t)(gchar *adapter,
+ gchar *device,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data);
+
+typedef void (*bluez_media_control_event_cb_t)(gchar *adapter,
+ gchar *device,
+ gchar *endpoint,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data);
+
+typedef void (*bluez_media_player_event_cb_t)(gchar *adapter,
+ gchar *device,
+ gchar *player,
+ bluez_event_t event,
+ GVariant *properties,
+ gpointer user_data);
+
+typedef void (*bluez_agent_event_cb_t)(gchar *device,
+ bluez_agent_event_t event,
+ GVariant *properties,
+ gpointer user_data);
+
+typedef void (*bluez_device_connect_cb_t)(gchar *device, gboolean status, gpointer user_data);
+
+typedef void (*bluez_device_pair_cb_t)(gchar *device, gboolean status, gpointer user_data);
+
+void bluez_add_adapter_event_callback(bluez_adapter_event_cb_t cb, gpointer user_data);
+
+void bluez_add_device_event_callback(bluez_device_event_cb_t cb, gpointer user_data);
+
+void bluez_add_media_control_event_callback(bluez_media_control_event_cb_t cb, gpointer user_data);
+
+void bluez_add_media_player_event_callback(bluez_media_player_event_cb_t cb, gpointer user_data);
+
+void bluez_add_agent_event_callback(bluez_agent_event_cb_t cb, gpointer user_data);
+
+void bluez_set_log_level(bluez_log_level_t level);
+
+gboolean bluez_init(gboolean register_agent,
+ gboolean autoconnect,
+ bluez_init_cb_t cb,
+ gpointer user_data);
+
+char *bluez_get_default_adapter(void);
+
+gboolean bluez_set_default_adapter(const char *adapter, char **adapter_new);
+
+gboolean bluez_get_managed_objects(GVariant **reply);
+
+gboolean bluez_get_adapters(GArray **reply);
+
+gboolean bluez_adapter_get_state(const char *adapter, GVariant **reply);
+
+gboolean bluez_adapter_get_devices(const char *adapter, GVariant **reply);
+
+gboolean bluez_adapter_set_discovery(const char *adapter, gboolean scan);
+
+gboolean bluez_adapter_set_discovery_filter(const char *adapter, gchar **uuids, gchar *transport);
+
+gboolean bluez_adapter_set_discoverable(const char *adapter, gboolean discoverable);
+
+gboolean bluez_adapter_set_powered(const char *adapter, gboolean powered);
+
+gboolean bluez_device_connect(const char *device,
+ const char *uuid,
+ bluez_device_connect_cb_t cb,
+ gpointer user_data);
+
+gboolean bluez_device_disconnect(const char *device, const char *uuid);
+
+gboolean bluez_device_pair(const char *device,
+ bluez_device_pair_cb_t cb,
+ gpointer user_data);
+
+gboolean bluez_cancel_pairing(void);
+
+gboolean bluez_confirm_pairing(const char *pincode_str);
+
+gboolean bluez_device_remove(const char *device);
+
+gboolean bluez_device_avrcp_controls(const char *device, bluez_media_control_t action);
+
+gboolean bluez_set_pincode(const char *pincode);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif /* BLUEZ_GLIB_H */
diff --git a/include/meson.build b/include/meson.build
new file mode 100644
index 0000000..40776e2
--- /dev/null
+++ b/include/meson.build
@@ -0,0 +1 @@
+install_headers('bluez-glib.h')
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..d09853c
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,16 @@
+project('bluez-glib', 'c', license : 'Apache-2.0')
+
+systemd_dep = dependency('systemd', version : '>=222')
+glib_deps = [dependency('glib-2.0'), dependency('gio-2.0'), dependency('gobject-2.0'), dependency('gio-unix-2.0')]
+
+inc = include_directories('include')
+
+subdir('include')
+subdir('src')
+
+pkg_mod = import('pkgconfig')
+pkg_mod.generate(libraries : lib,
+ version : '1.0',
+ name : 'libbluez-glib',
+ filebase : 'bluez-glib',
+ description : 'GLib helper library for using BlueZ.')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..397f787
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('build-tester', type : 'boolean', value : false)
diff --git a/src/api.c b/src/api.c
new file mode 100644
index 0000000..970deab
--- /dev/null
+++ b/src/api.c
@@ -0,0 +1,1543 @@
+/*
+ * Copyright 2018-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <pthread.h>
+#include <semaphore.h>
+
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include "bluez-glib.h"
+#include "common.h"
+#include "conf.h"
+#include "call_work.h"
+#include "bluez-call.h"
+#include "bluez-agent.h"
+
+typedef struct bluez_signal_callback_list_entry_t {
+ gpointer callback;
+ gpointer user_data;
+} callback_list_entry_t;
+
+typedef struct {
+ GMutex mutex;
+ GSList *list;
+} callback_list_t;
+
+callback_list_t bluez_adapter_callbacks;
+callback_list_t bluez_device_callbacks;
+callback_list_t bluez_media_control_callbacks;
+callback_list_t bluez_media_player_callbacks;
+
+// The global handler thread and BlueZ state
+static GThread *g_bluez_thread;
+static struct bluez_state *g_bluez_state;
+
+// Global log level
+static bluez_log_level_t g_bluez_log_level = BLUEZ_LOG_LEVEL_DEFAULT;
+
+static const char *g_bluez_log_level_names[BLUEZ_LOG_LEVEL_DEBUG + 1] = {
+ "ERROR",
+ "WARNING",
+ "INFO",
+ "DEBUG"
+};
+
+// Wrappers to hedge possible future abstractions
+static void bluez_set_state(struct bluez_state *ns)
+{
+ g_bluez_state = ns;
+}
+
+static struct bluez_state *bluez_get_state(void)
+{
+ return g_bluez_state;
+}
+
+EXPORT void bluez_set_log_level(bluez_log_level_t level)
+{
+ g_bluez_log_level = level;
+}
+
+void bluez_log(bluez_log_level_t level, const char *func, const char *format, ...)
+{
+ FILE *out = stdout;
+
+ if (level > g_bluez_log_level)
+ return;
+
+ if (level == BLUEZ_LOG_LEVEL_ERROR)
+ out = stderr;
+
+ va_list args;
+ va_start(args, format);
+ fprintf(out, "%s: %s: ", g_bluez_log_level_names[level], func);
+ gchar *format_line = g_strconcat(format, "\n", NULL);
+ vfprintf(out, format_line, args);
+ va_end(args);
+ g_free(format_line);
+}
+
+static void callback_add(callback_list_t *callbacks, gpointer callback, gpointer user_data)
+{
+ callback_list_entry_t *entry = NULL;
+
+ if(!callbacks)
+ return;
+
+ g_mutex_lock(&callbacks->mutex);
+ entry = g_malloc0(sizeof(*entry));
+ entry->callback = callback;
+ entry->user_data = user_data;
+ callbacks->list = g_slist_append(callbacks->list, entry);
+ g_mutex_unlock(&callbacks->mutex);
+}
+
+static void callback_remove(callback_list_t *callbacks, gpointer callback)
+{
+ callback_list_entry_t *entry = NULL;
+ GSList *list;
+
+ if(!(callbacks && callbacks->list))
+ return;
+
+ g_mutex_lock(&callbacks->mutex);
+ for (list = callbacks->list; list; list = g_slist_next(list)) {
+ entry = list->data;
+ if (entry->callback == callback)
+ break;
+ entry = NULL;
+ }
+ if (entry) {
+ callbacks->list = g_slist_remove(callbacks->list, entry);
+ g_free(entry);
+ }
+ g_mutex_unlock(&callbacks->mutex);
+}
+
+static void run_callbacks(callback_list_t *callbacks,
+ gchar *adapter,
+ gchar *device,
+ bluez_event_t event,
+ GVariant *properties)
+{
+ GSList *list;
+
+ if (!(adapter || device))
+ return;
+
+ g_mutex_lock(&callbacks->mutex);
+ for (list = callbacks->list; list; list = g_slist_next(list)) {
+ callback_list_entry_t *entry = list->data;
+ if (entry->callback) {
+ if (device) {
+ bluez_device_event_cb_t cb = (bluez_device_event_cb_t) entry->callback;
+ (*cb)(adapter, device, event, properties, entry->user_data);
+ } else {
+ bluez_adapter_event_cb_t cb = (bluez_adapter_event_cb_t) entry->callback;
+ (*cb)(adapter, event, properties, entry->user_data);
+ }
+ }
+ }
+ g_mutex_unlock(&callbacks->mutex);
+}
+
+static void run_media_callbacks(callback_list_t *callbacks,
+ gchar *adapter,
+ gchar *device,
+ gchar *endpoint,
+ gchar *player,
+ bluez_event_t event,
+ GVariant *properties)
+{
+ GSList *list;
+
+ if (!(endpoint || player))
+ return;
+
+ g_mutex_lock(&callbacks->mutex);
+ for (list = callbacks->list; list; list = g_slist_next(list)) {
+ callback_list_entry_t *entry = list->data;
+ if (entry->callback) {
+ if (player) {
+ bluez_media_player_event_cb_t cb = (bluez_media_player_event_cb_t) entry->callback;
+ (*cb)(adapter, device, player, event, properties, entry->user_data);
+ } else {
+ bluez_media_control_event_cb_t cb = (bluez_media_control_event_cb_t) entry->callback;
+ (*cb)(adapter, device, endpoint, event, properties, entry->user_data);
+ }
+ }
+ }
+ g_mutex_unlock(&callbacks->mutex);
+}
+
+EXPORT void bluez_add_adapter_event_callback(bluez_adapter_event_cb_t cb, gpointer user_data)
+{
+ if (!cb)
+ return;
+
+ callback_add(&bluez_adapter_callbacks, cb, user_data);
+}
+
+EXPORT void bluez_add_device_event_callback(bluez_device_event_cb_t cb, gpointer user_data)
+{
+ if (!cb)
+ return;
+
+ callback_add(&bluez_device_callbacks, cb, user_data);
+}
+
+EXPORT void bluez_add_media_control_event_callback(bluez_media_control_event_cb_t cb, gpointer user_data)
+{
+ if (!cb)
+ return;
+
+ callback_add(&bluez_media_control_callbacks, cb, user_data);
+}
+
+EXPORT void bluez_add_media_player_event_callback(bluez_media_player_event_cb_t cb, gpointer user_data)
+{
+ if (!cb)
+ return;
+
+ callback_add(&bluez_media_player_callbacks, cb, user_data);
+}
+
+static void mediaplayer1_set_path(struct bluez_state *ns, const char *path)
+{
+ if (ns->mediaplayer_path)
+ g_free(ns->mediaplayer_path);
+ ns->mediaplayer_path = g_strdup(path);
+}
+
+static void bluez_devices_signal_callback(GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ const gchar *path = NULL;
+ GVariantIter *array = NULL;
+ GVariant *var = NULL;
+ const gchar *key = NULL;
+
+#ifdef BLUEZ_GLIB_DEBUG
+ INFO("sender=%s", sender_name);
+ INFO("object_path=%s", object_path);
+ INFO("interface=%s", interface_name);
+ INFO("signal=%s", signal_name);
+ DEBUG("parameters: %s", g_variant_print(parameters, TRUE));
+#endif
+
+ if (!g_strcmp0(signal_name, "InterfacesAdded")) {
+
+ g_variant_get(parameters, "(&oa{sa{sv}})", &path, &array);
+
+ // no adapter or device in path
+ if (!g_strcmp0(path, BLUEZ_PATH))
+ return;
+
+ while (g_variant_iter_next(array, "{&s@a{sv}}", &key, &var)) {
+ if (!g_strcmp0(key, BLUEZ_DEVICE_INTERFACE)) {
+ DEBUG("device added!");
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ run_callbacks(&bluez_device_callbacks, adapter, device, BLUEZ_EVENT_ADD, var);
+ g_free(adapter);
+ g_free(device);
+
+ } else if (!g_strcmp0(key, BLUEZ_ADAPTER_INTERFACE)) {
+ DEBUG("adapter added!");
+ gchar *adapter = bluez_return_adapter(path);
+ run_callbacks(&bluez_adapter_callbacks, adapter, NULL, BLUEZ_EVENT_ADD, var);
+ g_free(adapter);
+
+ } else if (!g_strcmp0(key, BLUEZ_MEDIATRANSPORT_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ gchar *endpoint = bluez_return_endpoint(path);
+ DEBUG("media endpoint added!");
+ DEBUG("media endpoint path %s, key %s, endpoint %s",
+ path, key, endpoint);
+ run_media_callbacks(&bluez_media_control_callbacks,
+ adapter,
+ device,
+ endpoint,
+ NULL,
+ BLUEZ_EVENT_ADD,
+ var);
+ g_free(adapter);
+ g_free(device);
+ g_free(endpoint);
+
+ } else if (!g_strcmp0(key, BLUEZ_MEDIAPLAYER_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ gchar *player = find_index(path, 5);
+ DEBUG("media player removed!");
+ DEBUG("media player = %s", player);
+ run_media_callbacks(&bluez_media_player_callbacks,
+ adapter,
+ device,
+ NULL,
+ player,
+ BLUEZ_EVENT_ADD,
+ var);
+ g_free(adapter);
+ g_free(device);
+ g_free(player);
+ }
+ g_variant_unref(var);
+ }
+ g_variant_iter_free(array);
+
+ } else if (!g_strcmp0(signal_name, "InterfacesRemoved")) {
+
+ g_variant_get(parameters, "(&o@as)", &path, NULL);
+
+ if (is_mediatransport1_interface(path)) {
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ gchar *endpoint = bluez_return_endpoint(path);
+ DEBUG("media endpoint removed!");
+ DEBUG("media endpoint = %s", endpoint);
+ run_media_callbacks(&bluez_media_control_callbacks,
+ adapter,
+ device,
+ endpoint,
+ NULL,
+ BLUEZ_EVENT_REMOVE,
+ NULL);
+ g_free(adapter);
+ g_free(device);
+ g_free(endpoint);
+
+ } else if (is_mediaplayer1_interface(path)) {
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ gchar *player = find_index(path, 5);
+ DEBUG("media player removed!");
+ DEBUG("media player = %s", player);
+ run_media_callbacks(&bluez_media_player_callbacks,
+ adapter,
+ device,
+ NULL,
+ player,
+ BLUEZ_EVENT_REMOVE,
+ NULL);
+ g_free(adapter);
+ g_free(device);
+ g_free(player);
+
+ } else if (split_length(path) == 4) {
+ /* adapter removal */
+ DEBUG("adapter removed!");
+ gchar *adapter = bluez_return_adapter(path);
+ run_callbacks(&bluez_adapter_callbacks, adapter, NULL, BLUEZ_EVENT_REMOVE, NULL);
+ g_free(adapter);
+
+ } else if (split_length(path) == 5) {
+ /* device removal */
+ DEBUG("device removed!");
+ gchar *adapter = bluez_return_adapter(path);
+ gchar *device = bluez_return_device(path);
+ run_callbacks(&bluez_device_callbacks, adapter, device, BLUEZ_EVENT_REMOVE, NULL);
+ g_free(adapter);
+ g_free(device);
+
+ }
+ } else if (!g_strcmp0(signal_name, "PropertiesChanged")) {
+
+ g_variant_get(parameters, "(&s@a{sv}@as)", &path, &var, NULL);
+
+ if (!g_strcmp0(path, BLUEZ_DEVICE_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(object_path);
+ gchar *device = bluez_return_device(object_path);
+ run_callbacks(&bluez_device_callbacks, adapter, device, BLUEZ_EVENT_CHANGE, var);
+ g_free(adapter);
+ g_free(device);
+
+ } else if (!g_strcmp0(path, BLUEZ_ADAPTER_INTERFACE)) {
+ DEBUG("adapter changed!");
+ gchar *adapter = bluez_return_adapter(object_path);
+ run_callbacks(&bluez_adapter_callbacks, adapter, NULL, BLUEZ_EVENT_CHANGE, var);
+ g_free(adapter);
+
+ } else if (!g_strcmp0(path, BLUEZ_MEDIAPLAYER_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(object_path);
+ gchar *device = bluez_return_device(object_path);
+ gchar *player = find_index(object_path, 5);
+ DEBUG("media player changed!");
+ DEBUG("media player = %s", player);
+ run_media_callbacks(&bluez_media_player_callbacks,
+ adapter,
+ device,
+ NULL,
+ player,
+ BLUEZ_EVENT_CHANGE,
+ var);
+ g_free(adapter);
+ g_free(device);
+ g_free(player);
+
+ } else if (!g_strcmp0(path, BLUEZ_MEDIATRANSPORT_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(object_path);
+ gchar *device = bluez_return_device(object_path);
+ gchar *endpoint = bluez_return_endpoint(object_path);
+ DEBUG("media endpoint changed!");
+ DEBUG("media endpoint %s", endpoint);
+ run_media_callbacks(&bluez_media_control_callbacks,
+ adapter,
+ device,
+ endpoint,
+ NULL,
+ BLUEZ_EVENT_REMOVE,
+ var);
+ g_free(adapter);
+ g_free(device);
+ g_free(endpoint);
+
+ }
+ g_variant_unref(var);
+ }
+}
+
+// Returns number of adapters present
+static int bluez_select_init_adapter(void)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GArray *adapters = NULL;
+ gboolean rc;
+ int i, n = 0;
+
+ if (!(ns && ns->default_adapter))
+ return 0;
+
+ rc = bluez_get_adapters(&adapters);
+ if (!rc) {
+ return 0;
+ }
+
+ for(i = 0; i < adapters->len; i++) {
+ gchar *adapter = g_array_index(adapters, gchar*, i);
+ if (!g_strcmp0(adapter, ns->default_adapter)) {
+ DEBUG("found default adapter %s", adapter);
+ ns->adapter = ns->default_adapter;
+ break;
+ }
+ }
+ if (adapters->len && i == adapters->len) {
+ /* fallback to 1st available adapter */
+ ns->adapter = g_strdup(g_array_index(adapters, gchar*, 0));
+ INFO("default adapter %s not found, fell back to: %s",
+ ns->default_adapter, ns->adapter);
+ }
+ n = adapters->len;
+
+ // Clean up
+ for(i = 0; i < adapters->len; i++) {
+ g_free(g_array_index(adapters, gchar*, i));
+ }
+ g_array_unref(adapters);
+
+ return n;
+}
+
+static struct bluez_state *bluez_dbus_init(GMainLoop *loop, gboolean autoconnect)
+{
+ struct bluez_state *ns;
+ GError *error = NULL;
+
+ ns = g_try_malloc0(sizeof(*ns));
+ if (!ns) {
+ ERROR("out of memory allocating bluez state");
+ goto err_no_ns;
+ }
+
+ INFO("connecting to dbus");
+
+ ns->loop = loop;
+ ns->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
+ if (!ns->conn) {
+ if (error)
+ g_dbus_error_strip_remote_error(error);
+ ERROR("Cannot connect to D-Bus, %s",
+ error ? error->message : "unspecified");
+ g_error_free(error);
+ goto err_no_conn;
+
+ }
+
+ INFO("connected to dbus");
+
+ ns->device_sub = g_dbus_connection_signal_subscribe(
+ ns->conn,
+ BLUEZ_SERVICE,
+ NULL, /* interface */
+ NULL, /* member */
+ NULL, /* object path */
+ NULL, /* arg0 */
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ bluez_devices_signal_callback,
+ ns,
+ NULL);
+ if (!ns->device_sub) {
+ ERROR("Unable to subscribe to interface signals");
+ goto err_no_device_sub;
+ }
+
+ g_mutex_init(&ns->cw_mutex);
+ ns->next_cw_id = 1;
+
+ if (autoconnect)
+ g_timeout_add_seconds(5, bluez_autoconnect, ns);
+
+ INFO("done");
+ return ns;
+
+err_no_device_sub:
+ g_dbus_connection_close(ns->conn, NULL, NULL, NULL);
+err_no_conn:
+ g_free(ns);
+err_no_ns:
+ return NULL;
+}
+
+static void signal_init_done(struct init_data *id, gboolean rc)
+{
+ g_mutex_lock(&id->mutex);
+ id->init_done = TRUE;
+ id->rc = rc;
+ g_cond_signal(&id->cond);
+ g_mutex_unlock(&id->mutex);
+}
+
+static void bluez_cleanup(struct bluez_state *ns)
+{
+ g_dbus_connection_signal_unsubscribe(ns->conn, ns->device_sub);
+ g_dbus_connection_close(ns->conn, NULL, NULL, NULL);
+ g_free(ns);
+}
+
+typedef struct notifier_data {
+ struct bluez_state *ns;
+ bluez_init_cb_t cb;
+ gpointer cb_data;
+} notifier_data_t;
+
+// Helper function to trigger client init callback once the glib main loop
+// is running.
+static gboolean main_loop_start_notifier(gpointer user_data)
+{
+ if (!user_data) {
+ ERROR("Bad start notifier data!");
+
+ // Probably not much point returning TRUE to try again
+ return FALSE;
+ }
+
+ notifier_data_t *data = (notifier_data_t*) user_data;
+ DEBUG("running init callback");
+ if (data->cb && data->ns) {
+ bluez_init_cb_t cb = data->cb;
+ (*cb)(data->ns->adapter, TRUE, data->cb_data);
+ g_free(data);
+ }
+
+ // Let loop know we can be removed
+ return FALSE;
+}
+
+static gpointer bluez_handler_func(gpointer ptr)
+{
+ struct init_data *id = ptr;
+ struct bluez_state *ns;
+ GMainLoop *loop;
+ int num_adapters = 0;
+ unsigned int delay;
+ unsigned int attempt;
+ notifier_data_t *notifier_data;
+
+ g_atomic_rc_box_acquire(id);
+
+ // Save callback info for later use
+ notifier_data = g_malloc0(sizeof(*notifier_data));
+ if (!notifier_data) {
+ ERROR("Unable to alloc notifier data");
+ goto err_no_loop;
+ }
+ notifier_data->cb = id->cb;
+ notifier_data->cb_data = id->user_data;
+
+ loop = g_main_loop_new(NULL, FALSE);
+ if (!loop) {
+ ERROR("Unable to create main loop");
+ goto err_no_loop;
+ }
+
+ // Do BlueZ D-Bus related init
+ ns = bluez_dbus_init(loop, id->autoconnect);
+ if (!ns) {
+ ERROR("bluez_init() failed");
+ goto err_no_ns;
+ }
+
+ id->ns = ns;
+ bluez_set_state(ns);
+
+ ns->default_adapter = get_default_adapter();
+ if (!ns->default_adapter) {
+ ns->default_adapter = g_strdup(BLUEZ_DEFAULT_ADAPTER);
+ gboolean rc = set_default_adapter(BLUEZ_DEFAULT_ADAPTER);
+ if (!rc)
+ WARNING("Request to save default adapter to persistent storage failed ");
+ }
+
+ if (id->register_agent) {
+ gboolean rc = bluez_register_agent(id);
+ if (!rc) {
+ ERROR("bluetooth_register_agent() failed");
+ goto err_no_agent;
+ }
+ } else {
+ // Let main process know initialization is done
+ signal_init_done(id, TRUE);
+ }
+
+ // Cannot reference id after this point
+ g_atomic_rc_box_release(id);
+
+ // Wait for an adapter to appear
+ num_adapters = 0;
+ delay = 1;
+ attempt = 1;
+ while(num_adapters <= 0) {
+ num_adapters = bluez_select_init_adapter();
+ if (num_adapters > 0)
+ break;
+
+ // Back off querying rate after the first 60 seconds
+ if (attempt++ == 60)
+ delay = 10;
+
+ sleep(delay);
+ }
+
+ if (num_adapters > 0) {
+ notifier_data->ns = ns;
+ g_timeout_add_full(G_PRIORITY_DEFAULT,
+ 0,
+ main_loop_start_notifier,
+ notifier_data,
+ NULL);
+
+ DEBUG("calling g_main_loop_run");
+ g_main_loop_run(loop);
+ } else {
+ ERROR("bluez_select_init_adapter() failed");
+ }
+
+ g_main_loop_unref(ns->loop);
+
+ if (ns->agent_path)
+ bluez_unregister_agent(ns);
+
+ bluez_cleanup(ns);
+ bluez_set_state(NULL);
+
+ return NULL;
+
+err_no_agent:
+ bluez_cleanup(ns);
+
+err_no_ns:
+ g_main_loop_unref(loop);
+
+err_no_loop:
+ if (notifier_data && notifier_data->cb) {
+ (*notifier_data->cb)(NULL, FALSE, notifier_data->cb_data);
+ g_free(notifier_data);
+ }
+
+ signal_init_done(id, FALSE);
+ g_atomic_rc_box_release(id);
+
+ return NULL;
+}
+
+EXPORT gboolean bluez_init(gboolean register_agent,
+ gboolean autoconnect,
+ bluez_init_cb_t cb,
+ gpointer user_data)
+{
+ struct init_data *id = NULL;
+ gint64 end_time;
+ gboolean rc;
+ gboolean init_done;
+
+ id = g_atomic_rc_box_new0(struct init_data);
+ if (!id)
+ return -ENOMEM;
+ g_atomic_rc_box_acquire(id);
+
+ id->register_agent = register_agent;
+ id->autoconnect = autoconnect;
+ //id->init_done = FALSE;
+ id->init_done_cb = signal_init_done;
+ //id->rc = TRUE;
+ id->cb = cb;
+ id->user_data = user_data;
+ g_cond_init(&id->cond);
+ g_mutex_init(&id->mutex);
+
+ g_bluez_thread = g_thread_new("bluez_handler",
+ bluez_handler_func,
+ id);
+
+ INFO("waiting for init done");
+
+ /* wait maximum 10 seconds for init done */
+ end_time = g_get_monotonic_time () + 10 * G_TIME_SPAN_SECOND;
+ g_mutex_lock(&id->mutex);
+ while (!id->init_done) {
+ if (!g_cond_wait_until(&id->cond, &id->mutex, end_time))
+ break;
+ }
+ rc = id->rc;
+ init_done = id->init_done;
+ g_mutex_unlock(&id->mutex);
+ g_atomic_rc_box_release(id);
+
+ if (!init_done) {
+ ERROR("init timeout");
+ return FALSE;
+ }
+
+ if (!rc)
+ ERROR("init thread failed");
+ else
+ INFO("running");
+
+ return rc;
+}
+
+EXPORT char *bluez_get_default_adapter(void)
+{
+ struct bluez_state *ns = bluez_get_state();
+ if (!(ns && ns->adapter)) {
+ return NULL;
+ }
+ return ns->adapter;
+}
+
+EXPORT gboolean bluez_set_default_adapter(const char *adapter,
+ char **adapter_new)
+{
+ gboolean rc = TRUE;
+ char *adapter_default = get_default_adapter();
+
+ if (adapter) {
+ if (adapter_default && g_strcmp0(adapter_default, adapter)) {
+ rc = set_default_adapter(adapter);
+ if (!rc) {
+ WARNING("Request to save default adapter to persistent storage failed");
+ return FALSE;
+ }
+ }
+ if(rc && adapter_new)
+ *adapter_new = g_strdup(adapter);
+ } else if (adapter_default) {
+ *adapter_new = g_strdup(adapter_default);
+ } else {
+ ERROR("No default adapter");
+ rc = FALSE;
+ }
+ return rc;
+}
+
+EXPORT gboolean bluez_get_managed_objects(GVariant **reply)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+
+ if (!ns) {
+ ERROR("Invalid state");
+ return FALSE;
+ }
+ if (!reply)
+ return FALSE;
+
+ *reply = bluez_get_properties(ns,
+ BLUEZ_AT_OBJECT,
+ BLUEZ_OBJECT_PATH,
+ &error);
+ if (error) {
+ ERROR("bluez_get_properties error: %s", error->message);
+ g_clear_error(&error);
+ *reply = NULL;
+ }
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_get_adapters(GArray **reply)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GVariant *objects;
+ GError *error = NULL;
+
+ if (!ns) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+ if (!reply)
+ return FALSE;
+
+ objects = bluez_get_properties(ns,
+ BLUEZ_AT_OBJECT,
+ BLUEZ_OBJECT_PATH,
+ &error);
+ if (error) {
+ ERROR("get properties error: %s", error->message);
+ g_error_free(error);
+ *reply = NULL;
+ return FALSE;
+ }
+
+ // Iterate and pull out adapters
+ GVariantIter *array, *array2;
+ GVariant *var = NULL;
+ const char *interface, *path2 = NULL;
+ GArray *adapters = g_array_new(FALSE, FALSE, sizeof(gchar*));
+
+ g_variant_get(objects, "(a{oa{sa{sv}}})", &array);
+ while (g_variant_iter_loop(array, "{oa{sa{sv}}}", &path2, &array2)) {
+ while (g_variant_iter_loop(array2, "{&s@a{sv}}", &interface, &var)) {
+ if (!strcmp(interface, BLUEZ_ADAPTER_INTERFACE)) {
+ gchar *adapter = bluez_return_adapter(path2);
+ g_array_append_val(adapters, adapter);
+ break;
+ }
+ }
+ }
+ if (array2)
+ g_variant_iter_free(array2);
+ if (array)
+ g_variant_iter_free(array);
+
+ *reply = adapters;
+
+ g_variant_unref(objects);
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_get_state(const char *adapter, GVariant **reply)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ const char *adapter_path;
+ GVariant *properties = NULL;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+ if (!reply)
+ return FALSE;
+
+ adapter_path = BLUEZ_ROOT_PATH(adapter ? adapter : ns->adapter);
+ properties = bluez_get_properties(ns,
+ BLUEZ_AT_ADAPTER,
+ adapter_path,
+ &error);
+ if (error) {
+ ERROR("bluez_get_properties error: %s", error->message);
+ g_error_free(error);
+ *reply = NULL;
+ return FALSE;
+ }
+
+ // Pull properties out of tuple so caller does not have to
+ g_variant_get(properties, "(@a{sv})", reply);
+ g_variant_unref(properties);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_get_devices(const char *adapter, GVariant **reply)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GVariant *objects;
+ GError *error = NULL;
+ const char *target_adapter;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+ if (!reply)
+ return FALSE;
+
+ target_adapter = adapter ? adapter : ns->adapter;
+
+ objects = bluez_get_properties(ns,
+ BLUEZ_AT_OBJECT,
+ BLUEZ_OBJECT_PATH,
+ &error);
+ if (error) {
+ ERROR("bluez_get_properties error: %s", error->message);
+ g_error_free(error);
+ *reply = NULL;
+ return FALSE;
+ }
+
+ // Iterate and pull out devices
+ GVariantIter *array, *array2;
+ GVariant *var = NULL;
+ const char *interface, *path2 = NULL;
+ GVariantDict *devices_dict = g_variant_dict_new(NULL);
+ if (!devices_dict) {
+ ERROR("g_variant_dict_new failed");
+ g_variant_unref(objects);
+ *reply = NULL;
+ return FALSE;
+ }
+
+ g_variant_get(objects, "(a{oa{sa{sv}}})", &array);
+ while (g_variant_iter_loop(array, "{oa{sa{sv}}}", &path2, &array2)) {
+ gchar *device_adapter = bluez_return_adapter(path2);
+ if (!device_adapter || strcmp(device_adapter, target_adapter) != 0) {
+ g_free(device_adapter);
+ continue;
+ }
+ while (g_variant_iter_loop(array2, "{&s@a{sv}}", &interface, &var)) {
+ if (!strcmp(interface, BLUEZ_DEVICE_INTERFACE)) {
+ gchar *device = bluez_return_device(path2);
+ g_variant_dict_insert_value(devices_dict,
+ device,
+ var);
+ }
+ }
+ }
+ if (array2)
+ g_variant_iter_free(array2);
+ if (array)
+ g_variant_iter_free(array);
+
+ *reply = g_variant_dict_end(devices_dict);
+ g_variant_dict_unref(devices_dict);
+ g_variant_unref(objects);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_set_discovery(const char *adapter,
+ gboolean scan)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+
+ adapter = BLUEZ_ROOT_PATH(adapter ? adapter : ns->adapter);
+
+ GVariant *reply = bluez_call(ns, BLUEZ_AT_ADAPTER, adapter,
+ scan ? "StartDiscovery" : "StopDiscovery",
+ NULL, &error);
+ if (!reply) {
+ ERROR("adapter %s method %s error: %s",
+ adapter, "Scan", BLUEZ_ERRMSG(error));
+ g_error_free(error);
+ return FALSE;
+ }
+ g_variant_unref(reply);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_set_discovery_filter(const char *adapter,
+ gchar **uuids,
+ gchar *transport)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ GVariantBuilder builder;
+ GVariant *filter, *reply;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+
+ if (!(uuids || transport)) {
+ return FALSE;
+ }
+
+ adapter = BLUEZ_ROOT_PATH(adapter ? adapter : ns->adapter);
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+
+ if (uuids && *uuids != NULL) {
+ g_variant_builder_add(&builder, "{sv}", "UUIDs",
+ g_variant_new_strv((const gchar * const *) uuids, -1));
+ }
+
+ if (transport) {
+ g_variant_builder_add(&builder, "{sv}", "Transport",
+ g_variant_new_string(transport));
+ }
+
+ filter = g_variant_builder_end(&builder);
+
+ reply = bluez_call(ns, BLUEZ_AT_ADAPTER, adapter,
+ "SetDiscoveryFilter",
+ g_variant_new("(@a{sv})", filter), &error);
+ if (!reply) {
+ ERROR("adapter %s method %s error: %s",
+ adapter, "SetDiscoveryFilter", BLUEZ_ERRMSG(error));
+ g_error_free(error);
+ return FALSE;
+ }
+
+ g_variant_unref(reply);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_set_discoverable(const char *adapter,
+ gboolean discoverable)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ gboolean rc;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+
+ adapter = BLUEZ_ROOT_PATH(adapter ? adapter : ns->adapter);
+
+ DEBUG("discoverable = %d", (int) discoverable);
+ rc = bluez_set_boolean_property(ns, BLUEZ_AT_ADAPTER, adapter,
+ "Discoverable", discoverable, &error);
+ if (!rc) {
+ ERROR("adapter %s set_property %s error: %s",
+ adapter, "Discoverable", BLUEZ_ERRMSG(error));
+ g_error_free(error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+EXPORT gboolean bluez_adapter_set_powered(const char *adapter,
+ gboolean powered)
+{
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ gboolean rc;
+
+ if (!ns || (!adapter && !ns->adapter)) {
+ ERROR("No adapter");
+ return FALSE;
+ }
+
+ adapter = BLUEZ_ROOT_PATH(adapter ? adapter : ns->adapter);
+
+ rc = bluez_set_boolean_property(ns, BLUEZ_AT_ADAPTER, adapter,
+ "Powered", powered, &error);
+ if (!rc) {
+ ERROR("adapter %s set_property %s error: %s",
+ adapter, "Powered", BLUEZ_ERRMSG(error));
+ g_error_free(error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gchar *get_bluez_path(const char *adapter, const char *device)
+{
+ struct bluez_state *ns = bluez_get_state();
+ const char *tmp;
+
+ if (!ns || (!adapter && !ns->adapter))
+ return NULL;
+
+ if (!device)
+ return NULL;
+
+ tmp = device;
+
+ /* Stop the dbus call from segfaulting from special characters */
+ for (; *tmp; tmp++) {
+ if (!g_ascii_isalnum(*tmp) && *tmp != '_') {
+ ERROR("Invalid device parameter");
+ return NULL;
+ }
+ }
+
+ call_work_lock(ns);
+ adapter = adapter ? adapter : ns->adapter;
+ call_work_unlock(ns);
+
+ return g_strconcat("/org/bluez/", adapter, "/", device, NULL);
+}
+
+static void connect_service_callback(void *user_data,
+ GVariant *result,
+ GError **error)
+{
+ struct call_work *cw = user_data;
+ struct bluez_state *ns = cw->ns;
+ gboolean status = TRUE;
+
+ bluez_decode_call_error(ns,
+ cw->access_type, cw->type_arg, cw->bluez_method,
+ error);
+ if (error && *error) {
+ ERROR("Connect error: %s", (*error)->message);
+ status = FALSE;
+ }
+
+ if (result)
+ g_variant_unref(result);
+
+ // Run callback
+ if (cw->request_cb) {
+ bluez_device_connect_cb_t cb = (bluez_device_connect_cb_t) cw->request_cb;
+ gchar *device = g_strdup(cw->type_arg);
+ (*cb)(device, status, cw->request_user_data);
+ }
+
+ call_work_destroy(cw);
+}
+
+EXPORT gboolean bluez_device_connect(const char *device,
+ const char *uuid,
+ bluez_device_connect_cb_t cb,
+ gpointer user_data)
+{
+ gboolean rc = TRUE;
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ struct call_work *cw;
+ gchar *device_path;
+
+ device_path = get_bluez_path(NULL, device);
+ if (!device) {
+ ERROR("No path given");
+ return FALSE;
+ }
+
+ cw = call_work_create(ns, BLUEZ_AT_DEVICE, device_path,
+ "connect_service", "Connect", &error);
+ if (!cw) {
+ ERROR("can't queue work %s", error->message);
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free;
+ }
+
+ // Set callback hook
+ cw->request_cb = cb;
+ cw->request_user_data = user_data;
+
+ if (uuid) {
+ /* connect single profile */
+ cw->cpw = bluez_call_async(ns, BLUEZ_AT_DEVICE, device_path,
+ "ConnectProfile", g_variant_new("(&s)", uuid),
+ &error,
+ connect_service_callback, cw);
+ } else {
+ cw->cpw = bluez_call_async(ns, BLUEZ_AT_DEVICE, device_path,
+ "Connect", NULL,
+ &error,
+ connect_service_callback, cw);
+ }
+ if (!cw->cpw) {
+ ERROR("Connection error: %s", error->message);
+ call_work_destroy(cw);
+ g_error_free(error);
+ rc = FALSE;
+ /* fall-thru */
+ }
+
+out_free:
+ g_free(device_path);
+
+ return rc;
+}
+
+EXPORT gboolean bluez_device_disconnect(const char *device,
+ const char *uuid)
+{
+ gboolean rc = TRUE;
+ struct bluez_state *ns = bluez_get_state();
+ GVariant *reply = NULL;
+ GError *error = NULL;
+ gchar *device_path;
+
+ device_path = get_bluez_path(NULL, device);
+ if (!device_path) {
+ ERROR("No device given to disconnect");
+ return FALSE;
+ }
+ DEBUG("device = %s, device_path = %s", device, device_path);
+
+ if (uuid) {
+ /* Disconnect single profile */
+ reply = bluez_call(ns, BLUEZ_AT_DEVICE, device_path,
+ "DisconnectProfile", g_variant_new("(&s)", uuid),
+ &error);
+ } else {
+ reply = bluez_call(ns, BLUEZ_AT_DEVICE, device_path,
+ "Disconnect", NULL,
+ &error);
+ }
+
+ if (!reply) {
+ ERROR("Disconnect error: %s", BLUEZ_ERRMSG(error));
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free;
+ }
+
+ g_variant_unref(reply);
+
+out_free:
+ g_free(device_path);
+
+ return rc;
+}
+
+static void pair_service_callback(void *user_data,
+ GVariant *result,
+ GError **error)
+{
+ struct call_work *cw = user_data;
+ struct bluez_state *ns = cw->ns;
+ gboolean status = TRUE;
+
+ bluez_decode_call_error(ns,
+ cw->access_type, cw->type_arg, cw->bluez_method,
+ error);
+ if (error && *error) {
+ ERROR("Connect error: %s", (*error)->message);
+ status = FALSE;
+ }
+
+ if (result)
+ g_variant_unref(result);
+
+ // Run callback
+ if (cw->request_cb) {
+ bluez_device_pair_cb_t cb = (bluez_device_pair_cb_t) cw->request_cb;
+ gchar *device = g_strdup(cw->type_arg);
+ (*cb)(device, status, user_data);
+ }
+
+ call_work_destroy(cw);
+}
+
+EXPORT gboolean bluez_device_pair(const char *device,
+ bluez_device_pair_cb_t cb,
+ gpointer user_data)
+{
+ gboolean rc = TRUE;
+ struct bluez_state *ns = bluez_get_state();
+ GError *error = NULL;
+ gchar *device_path;
+ struct call_work *cw;
+
+ device_path = get_bluez_path(NULL, device);
+ if (!device_path) {
+ ERROR("No path given");
+ return FALSE;
+ }
+
+ cw = call_work_create(ns, BLUEZ_AT_DEVICE, device_path,
+ "pair_device", "Pair",
+ &error);
+ if (!cw) {
+ ERROR("can't queue work %s", error->message);
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free;
+ }
+
+ // Set callback hook
+ cw->request_cb = cb;
+ cw->request_user_data = user_data;
+
+ cw->agent_data.fixed_pincode = get_pincode();
+
+ cw->cpw = bluez_call_async(ns, BLUEZ_AT_DEVICE, device_path,
+ "Pair", NULL,
+ &error,
+ pair_service_callback, cw);
+ if (!cw->cpw) {
+ ERROR("Pairing error: %s", error->message);
+ call_work_destroy(cw);
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free;
+ }
+
+out_free:
+ g_free(device_path);
+
+ return rc;
+}
+
+EXPORT gboolean bluez_cancel_pairing(void)
+{
+ struct bluez_state *ns = bluez_get_state();
+ struct call_work *cw;
+ GVariant *reply = NULL;
+ GError *error = NULL;
+ gchar *device_path;
+
+ call_work_lock(ns);
+
+ cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation");
+ if (!cw) {
+ call_work_unlock(ns);
+ ERROR("No pairing in progress");
+ return FALSE;
+ }
+
+ device_path = cw->agent_data.device_path;
+ reply = bluez_call(ns, BLUEZ_AT_DEVICE, device_path,
+ "CancelPairing", NULL,
+ &error);
+ if (!reply) {
+ call_work_unlock(ns);
+ ERROR("device %s method %s error: %s",
+ device_path, "CancelPairing", error->message);
+ g_error_free(error);
+ return FALSE;
+ }
+
+ call_work_unlock(ns);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_confirm_pairing(const char *pincode_str)
+{
+ gboolean rc = TRUE;
+ struct bluez_state *ns = bluez_get_state();
+ struct call_work *cw;
+ int pin = -1;
+
+ if (pincode_str)
+ pin = (int) strtol(pincode_str, NULL, 10);
+
+ // GSM - FIXME - this seems broken wrt a pin of all zeroes?
+ if (!pincode_str || !pin) {
+ ERROR("No pincode parameter");
+ return FALSE;
+ }
+
+ call_work_lock(ns);
+ cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation");
+ if (!cw) {
+ call_work_unlock(ns);
+ ERROR("No pairing in progress");
+ return FALSE;
+ }
+
+ if (pin == cw->agent_data.pin_code) {
+ g_dbus_method_invocation_return_value(cw->invocation, NULL);
+ INFO("pairing confirmed");
+ } else {
+ g_dbus_method_invocation_return_dbus_error(cw->invocation,
+ "org.bluez.Error.Rejected",
+ "No connection pending");
+ ERROR("pairing failed");
+ rc = FALSE;
+ }
+
+ call_work_destroy_unlocked(cw);
+ call_work_unlock(ns);
+ return rc;
+}
+
+EXPORT gboolean bluez_device_remove(const char *device)
+{
+ gboolean rc = TRUE;
+ struct bluez_state *ns = bluez_get_state();
+ GVariant *reply;
+ GError *error = NULL;
+ const char *adapter = NULL;
+
+ gchar *device_path = get_bluez_path(NULL, device);
+ if (!device_path) {
+ ERROR("No path given");
+ return FALSE;
+ }
+
+ GVariant *val = bluez_get_property(ns, BLUEZ_AT_DEVICE, device_path, "Adapter", &error);
+ if (error) {
+ // This can happen if a device is removed while pairing, so unless
+ // an additional check for device presence is done first, it should
+ // not be logged as a definite error.
+ WARNING("adapter not found for device %s error: %s",
+ device, error->message);
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free;
+ }
+
+ adapter = g_variant_get_string(val, NULL);
+ if (!adapter) {
+ ERROR("adapter invalid for device %s", device);
+ rc = FALSE;
+ goto out_free_val;
+ }
+
+ reply = bluez_call(ns, BLUEZ_AT_ADAPTER, adapter,
+ "RemoveDevice", g_variant_new("(o)", device_path),
+ &error);
+ if (error) {
+ ERROR("device %s method %s error: %s",
+ device_path, "RemoveDevice", error->message);
+ g_error_free(error);
+ rc = FALSE;
+ goto out_free_val;
+ }
+ g_variant_unref(reply);
+
+ INFO("device %s removed", device_path);
+
+out_free_val:
+ g_variant_unref(val);
+out_free:
+ g_free(device_path);
+
+ return rc;
+}
+
+static void mediaplayer1_connect_disconnect(struct bluez_state *ns,
+ const gchar *player,
+ int state)
+{
+ GVariant *reply;
+ gchar *path = g_strdup(player);
+ const char *uuids[] = {
+ "0000110a-0000-1000-8000-00805f9b34fb",
+ "0000110e-0000-1000-8000-00805f9b34fb",
+ NULL
+ };
+ const char **tmp = (const char **) uuids;
+
+ *g_strrstr(path, "/") = '\0';
+
+ for (; *tmp; tmp++) {
+ reply = bluez_call(ns, BLUEZ_AT_DEVICE, path,
+ state ? "ConnectProfile" : "DisconnectProfile",
+ g_variant_new("(&s)", *tmp), NULL);
+ if (!reply)
+ break;
+ g_variant_unref(reply);
+ }
+
+ g_free(path);
+}
+
+EXPORT gboolean bluez_device_avrcp_controls(const char *device,
+ bluez_media_control_t action)
+{
+ struct bluez_state *ns = bluez_get_state();
+ const char *action_names[BLUEZ_MEDIA_CONTROL_REWIND + 1] = {
+ "Connect", "Disconnect", "Play", "Pause", "Stop",
+ "Next", "Previous", "FastForward", "Rewind"
+ };
+ const char *action_str = action_names[action];
+
+ gchar *player = NULL;
+ gchar *device_path = get_bluez_path(NULL, device);
+ if (device_path) {
+ // TODO: handle multiple players per device
+ GVariant *val = bluez_get_property(ns, BLUEZ_AT_MEDIACONTROL, device_path, "Player", NULL);
+ if (val) {
+ player = g_variant_get_string(val, NULL);
+ g_variant_unref(val);
+ }
+ if (!player)
+ player = g_strconcat(device_path, "/", BLUEZ_DEFAULT_PLAYER, NULL);
+
+ g_free(device_path);
+ } else {
+ player = g_strdup(ns->mediaplayer_path);
+ }
+
+ if (!player) {
+ ERROR("No path given");
+ return FALSE;
+ }
+
+ if (action == BLUEZ_MEDIA_CONTROL_CONNECT || action == BLUEZ_MEDIA_CONTROL_DISCONNECT) {
+ mediaplayer1_connect_disconnect(ns,
+ player,
+ action == BLUEZ_MEDIA_CONTROL_CONNECT);
+ goto out_success;
+ }
+
+ GError *error = NULL;
+ GVariant *reply = bluez_call(ns, BLUEZ_AT_MEDIAPLAYER, player, action_str, NULL, &error);
+ if (!reply) {
+ ERROR("mediaplayer %s method %s error: %s",
+ player, action_str, BLUEZ_ERRMSG(error));
+ g_free(player);
+ g_error_free(error);
+ return FALSE;
+ }
+ g_variant_unref(reply);
+
+out_success:
+ g_free(player);
+
+ return TRUE;
+}
+
+EXPORT gboolean bluez_set_pincode(const char *pincode)
+{
+ gboolean rc = TRUE;
+ char *error = NULL;
+
+ rc = set_pincode(pincode, &error);
+ if (rc) {
+ ERROR("%s", error);
+ }
+
+ return rc;
+}
diff --git a/src/bluez-agent.c b/src/bluez-agent.c
new file mode 100644
index 0000000..197177e
--- /dev/null
+++ b/src/bluez-agent.c
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2018,2020-2021 Konsulko Group
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <pthread.h>
+#include <semaphore.h>
+
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include "common.h"
+#include "call_work.h"
+#include "bluez-glib.h"
+#include "bluez-call.h"
+
+static bluez_agent_event_cb_t agent_event_cb = NULL;
+static gpointer agent_event_cb_data = NULL;
+static GMutex agent_event_cb_mutex;
+
+EXPORT void bluez_add_agent_event_callback(bluez_agent_event_cb_t cb, gpointer user_data)
+{
+ g_mutex_lock(&agent_event_cb_mutex);
+ if (agent_event_cb == NULL) {
+ agent_event_cb = cb;
+ agent_event_cb_data = user_data;
+ } else {
+ ERROR("Agent event callback already set");
+ }
+ g_mutex_unlock(&agent_event_cb_mutex);
+}
+
+static void run_callback(gchar *device,
+ bluez_agent_event_t event,
+ GVariant *properties)
+{
+ g_mutex_lock(&agent_event_cb_mutex);
+ if (agent_event_cb) {
+ (*agent_event_cb)(device, event, properties, agent_event_cb_data);
+ }
+ g_mutex_unlock(&agent_event_cb_mutex);
+}
+
+
+/* Introspection data for the agent service */
+static const gchar introspection_xml[] =
+"<node>"
+" <interface name='org.bluez.Agent1'>"
+" <method name='RequestPinCode'>"
+" <arg name='device' direction='in' type='o'/>"
+" <arg name='pincode' direction='out' type='s'/>"
+" </method>"
+" <method name='RequestConfirmation'>"
+" <arg name='device' direction='in' type='o'/>"
+" <arg name='passkey' direction='in' type='u'/>"
+" </method>"
+" <method name='AuthorizeService'>"
+" <arg name='device' direction='in' type='o'/>"
+" <arg name='uuid' direction='in' type='s'/>"
+" </method>"
+" <method name='Cancel'>"
+" </method>"
+" </interface>"
+"</node>";
+
+static void handle_method_call(
+ GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ struct bluez_state *ns = user_data;
+ struct call_work *cw;
+ GError *error = NULL;
+ const gchar *path = NULL;
+ char *device = NULL;
+
+#ifdef BLUEZ_GLIB_DEBUG
+ INFO("sender=%s", sender_name);
+ INFO("object_path=%s", object_path);
+ INFO("interface=%s", interface_name);
+ INFO("method=%s", method_name);
+ DEBUG("parameters: %s", g_variant_print(parameters, TRUE));
+#endif
+
+ if (!g_strcmp0(method_name, "RequestConfirmation")) {
+ int pin;
+
+ g_variant_get(parameters, "(ou)", &path, &pin);
+
+ call_work_lock(ns);
+
+ /* can only occur while a pair is issued */
+ cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation");
+
+ /* if nothing is pending return an error */
+ /* TODO: allow client side pairing */
+ if (!cw)
+ cw = call_work_create_unlocked(ns, "device", NULL,
+ "RequestConfirmation", NULL, &error);
+
+ if (!cw) {
+ call_work_unlock(ns);
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.bluez.Error.Rejected",
+ "No connection pending");
+ return;
+ }
+
+ cw->agent_data.pin_code = pin;
+ cw->agent_data.device_path = g_strdup(path);
+ cw->invocation = invocation;
+
+ call_work_unlock(ns);
+
+ run_callback(path, BLUEZ_AGENT_EVENT_REQUEST_CONFIRMATION, parameters);
+
+ return;
+
+ } else if (!g_strcmp0(method_name, "RequestPinCode")) {
+
+ g_variant_get(parameters, "(o)", &device);
+
+ call_work_lock(ns);
+ cw = call_work_lookup_unlocked(ns, BLUEZ_AT_DEVICE, device, "pair_device");
+
+ if (!cw)
+ g_dbus_method_invocation_return_dbus_error(invocation, "org.bluez.Error.Rejected", "No connection pending");
+ else
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", cw->agent_data.fixed_pincode));
+
+ call_work_unlock(ns);
+
+ return;
+
+ } else if (!g_strcmp0(method_name, "AuthorizeService")) {
+#if 0
+ // TODO: add some service validation here.
+ //g_variant_get(parameters, "(os)", &path, &service);
+#endif
+ return g_dbus_method_invocation_return_value(invocation, NULL);
+ } else if (!g_strcmp0(method_name, "Cancel")) {
+
+ call_work_lock(ns);
+
+ /* can only occur while a pair is issued */
+ cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation");
+ if (!cw) {
+ call_work_unlock(ns);
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.bluez.Error.Rejected",
+ "No connection pending");
+ return;
+ }
+
+ run_callback(path, BLUEZ_AGENT_EVENT_CANCELLED_PAIRING, parameters);
+
+ call_work_destroy_unlocked(cw);
+ call_work_unlock(ns);
+
+ return g_dbus_method_invocation_return_value(invocation, NULL);
+ }
+
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.freedesktop.DBus.Error.UnknownMethod",
+ "Unknown method");
+}
+
+static const GDBusInterfaceVTable interface_vtable = {
+ .method_call = handle_method_call,
+ .get_property = NULL,
+ .set_property = NULL,
+};
+
+static void on_bus_acquired(GDBusConnection *connection,
+ const gchar *name, gpointer user_data)
+{
+ struct init_data *id = user_data;
+ struct bluez_state *ns = id->ns;
+ GVariant *result;
+ GError *error = NULL;
+
+ INFO("agent bus acquired - registering %s", ns->agent_path);
+
+ ns->registration_id = g_dbus_connection_register_object(connection,
+ ns->agent_path,
+ ns->introspection_data->interfaces[0],
+ &interface_vtable,
+ ns, /* user data */
+ NULL, /* user_data_free_func */
+ NULL);
+
+ if (!ns->registration_id) {
+ ERROR("failed to register agent to dbus");
+ goto err_unable_to_register_bus;
+
+ }
+
+ result = bluez_call(ns, BLUEZ_AT_AGENTMANAGER, NULL,
+ "RegisterAgent",
+ g_variant_new("(os)", ns->agent_path, "KeyboardDisplay"),
+ &error);
+ if (!result) {
+ ERROR("failed to register agent to bluez");
+ goto err_unable_to_register_bluez;
+ }
+ g_variant_unref(result);
+
+ result = bluez_call(ns, BLUEZ_AT_AGENTMANAGER, NULL,
+ "RequestDefaultAgent",
+ g_variant_new("(o)", ns->agent_path),
+ &error);
+ if (!result) {
+ ERROR("failed to request default agent to bluez");
+ goto err_unable_to_request_default_agent_bluez;
+ }
+ g_variant_unref(result);
+
+ ns->agent_registered = TRUE;
+
+ INFO("agent registered at %s", ns->agent_path);
+ if (id->init_done_cb)
+ (*id->init_done_cb)(id, TRUE);
+
+ DEBUG("exit!");
+ return;
+
+err_unable_to_request_default_agent_bluez:
+ result = bluez_call(ns, BLUEZ_AT_AGENTMANAGER, NULL,
+ "UnregisterAgent",
+ g_variant_new("(o)", ns->agent_path),
+ &error);
+
+err_unable_to_register_bluez:
+ g_dbus_connection_unregister_object(ns->conn, ns->registration_id);
+ ns->registration_id = 0;
+
+err_unable_to_register_bus:
+ if (id->init_done_cb)
+ (*id->init_done_cb)(id, FALSE);
+ return;
+}
+
+gboolean bluez_register_agent(struct init_data *id)
+{
+ struct bluez_state *ns = id->ns;
+
+ ns->agent_path = g_strdup_printf("%s/agent%d",
+ BLUEZ_PATH, getpid());
+ if (!ns->agent_path) {
+ ERROR("can't create agent path");
+ goto out_no_agent_path;
+ }
+
+ ns->introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL);
+ if (!ns->introspection_data) {
+ ERROR("can't create introspection data");
+ goto out_no_introspection_data;
+ }
+
+ ns->agent_id = g_bus_own_name(G_BUS_TYPE_SYSTEM,
+ BLUEZ_AGENT_INTERFACE,
+ G_BUS_NAME_OWNER_FLAGS_REPLACE | G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT,
+ on_bus_acquired,
+ NULL,
+ NULL,
+ id,
+ NULL);
+ if (!ns->agent_id) {
+ ERROR("can't create agent bus instance");
+ goto out_no_bus_name;
+ }
+
+ return TRUE;
+
+out_no_bus_name:
+ g_dbus_node_info_unref(ns->introspection_data);
+out_no_introspection_data:
+ g_free(ns->agent_path);
+out_no_agent_path:
+ return FALSE;
+}
+
+void bluez_unregister_agent(struct bluez_state *ns)
+{
+ g_bus_unown_name(ns->agent_id);
+ g_dbus_node_info_unref(ns->introspection_data);
+ g_free(ns->agent_path);
+}
diff --git a/src/bluez-agent.h b/src/bluez-agent.h
new file mode 100644
index 0000000..e9902ef
--- /dev/null
+++ b/src/bluez-agent.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUEZ_AGENT_H
+#define BLUEZ_AGENT_H
+
+#define _GNU_SOURCE
+#include <glib.h>
+/*
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+*/
+
+#include "common.h"
+
+struct agent_data {
+ int pin_code;
+ gchar *fixed_pincode;
+ gchar *device_path;
+};
+
+gboolean bluez_register_agent(struct init_data *id);
+
+void bluez_unregister_agent(struct bluez_state *ns);
+
+#endif /* BLUEZ_AGENT_H */
diff --git a/src/bluez-call.c b/src/bluez-call.c
new file mode 100644
index 0000000..86f9b9c
--- /dev/null
+++ b/src/bluez-call.c
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2018,2020-2021 Konsulko Group
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <pthread.h>
+#include <semaphore.h>
+
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include "bluez-call.h"
+#include "common.h"
+
+G_DEFINE_QUARK(bluez-error-quark, bluez_error)
+
+
+void bluez_decode_call_error(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ GError **error)
+{
+ if (!error || !*error)
+ return;
+
+ if (strstr((*error)->message,
+ "org.freedesktop.DBus.Error.UnknownObject")) {
+
+ if (!strcmp(method, "Set") ||
+ !strcmp(method, "Get") ||
+ !strcmp(method, "GetAll")) {
+
+ g_clear_error(error);
+ g_set_error(error, BLUEZ_ERROR,
+ BLUEZ_ERROR_UNKNOWN_PROPERTY,
+ "unknown %s property on %s",
+ access_type, type_arg);
+
+ } else if (!strcmp(method, "Connect") ||
+ !strcmp(method, "ConnectProfile") ||
+ !strcmp(method, "Disconnect") ||
+ !strcmp(method, "DisconnectProfile") ||
+ !strcmp(method, "Pair") ||
+ !strcmp(method, "Unpair") ||
+ !strcmp(method, "RemoveDevice")) {
+
+ g_clear_error(error);
+ g_set_error(error, BLUEZ_ERROR,
+ BLUEZ_ERROR_UNKNOWN_SERVICE,
+ "unknown service %s",
+ type_arg);
+
+ } else if (!strcmp(method, "StartDiscovery") ||
+ !strcmp(method, "StopDiscovery") ||
+ !strcmp(method, "SetDiscoveryFilter") ||
+ !strcmp(method, "RegisterAgent")) {
+
+ g_clear_error(error);
+ g_set_error(error, BLUEZ_ERROR,
+ BLUEZ_ERROR_UNKNOWN_SERVICE,
+ "unknown service %s",
+ type_arg);
+ } else if (!strcmp(method, "Play") ||
+ !strcmp(method, "Pause") ||
+ !strcmp(method, "Stop") ||
+ !strcmp(method, "Next") ||
+ !strcmp(method, "Previous") ||
+ !strcmp(method, "FastForward") ||
+ !strcmp(method, "Rewind")) {
+
+ g_clear_error(error);
+ g_set_error(error, BLUEZ_ERROR,
+ BLUEZ_ERROR_UNKNOWN_PROPERTY,
+ "unknown method %s",
+ method);
+ }
+ }
+}
+
+GVariant *bluez_call(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ const char *method,
+ GVariant *params,
+ GError **error)
+{
+ const char *interface;
+ GVariant *reply;
+
+ if (!path && (!strcmp(access_type, BLUEZ_AT_DEVICE) ||
+ !strcmp(access_type, BLUEZ_AT_ADAPTER) ||
+ !strcmp(access_type, BLUEZ_AT_MEDIAPLAYER))) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_MISSING_ARGUMENT,
+ "missing %s argument",
+ access_type);
+ return NULL;
+ }
+
+ if (!strcmp(access_type, BLUEZ_AT_DEVICE)) {
+ interface = BLUEZ_DEVICE_INTERFACE;
+ } else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) {
+ interface = BLUEZ_ADAPTER_INTERFACE;
+ } else if (!strcmp(access_type, BLUEZ_AT_AGENTMANAGER)) {
+ path = BLUEZ_PATH;
+ interface = BLUEZ_AGENTMANAGER_INTERFACE;
+ } else if (!strcmp(access_type, BLUEZ_AT_MEDIAPLAYER)) {
+ interface = BLUEZ_MEDIAPLAYER_INTERFACE;
+ } else {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ "illegal %s argument",
+ access_type);
+ return NULL;
+ }
+
+ reply = g_dbus_connection_call_sync(ns->conn,
+ BLUEZ_SERVICE, path, interface, method, params,
+ NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT,
+ NULL, error);
+ bluez_decode_call_error(ns, access_type, path, method,error);
+ if (!reply && error) {
+ if (*error)
+ g_dbus_error_strip_remote_error(*error);
+ ERROR("Error calling %s%s%s %s method %s",
+ access_type,
+ path ? "/" : "",
+ path ? path : "",
+ method,
+ error && *error ? (*error)->message :
+ "unspecified");
+ }
+
+ return reply;
+}
+
+static void bluez_call_async_ready(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ struct bluez_pending_work *cpw = user_data;
+ struct bluez_state *ns = cpw->ns;
+ GVariant *result;
+ GError *error = NULL;
+
+ result = g_dbus_connection_call_finish(ns->conn, res, &error);
+
+ cpw->callback(cpw->user_data, result, &error);
+
+ g_clear_error(&error);
+ g_cancellable_reset(cpw->cancel);
+ g_free(cpw);
+}
+
+void bluez_cancel_call(struct bluez_state *ns,
+ struct bluez_pending_work *cpw)
+{
+ g_cancellable_cancel(cpw->cancel);
+}
+
+struct bluez_pending_work *
+bluez_call_async(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ GVariant *params,
+ GError **error,
+ void (*callback)(void *user_data, GVariant *result, GError **error),
+ void *user_data)
+{
+ const char *path;
+ const char *interface;
+ struct bluez_pending_work *cpw;
+
+ if (!type_arg && (!strcmp(access_type, BLUEZ_AT_DEVICE) ||
+ !strcmp(access_type, BLUEZ_AT_ADAPTER))) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_MISSING_ARGUMENT,
+ "missing %s argument",
+ access_type);
+ return NULL;
+ }
+
+ if (!strcmp(access_type, BLUEZ_AT_DEVICE)) {
+ path = type_arg;
+ interface = BLUEZ_DEVICE_INTERFACE;
+ } else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) {
+ path = type_arg;
+ interface = BLUEZ_ADAPTER_INTERFACE;
+ } else {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ "illegal %s argument",
+ access_type);
+ return NULL;
+ }
+
+ cpw = g_malloc(sizeof(*cpw));
+ if (!cpw) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_OUT_OF_MEMORY,
+ "out of memory");
+ return NULL;
+ }
+ cpw->ns = ns;
+ cpw->user_data = user_data;
+ cpw->cancel = g_cancellable_new();
+ if (!cpw->cancel) {
+ g_free(cpw);
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_OUT_OF_MEMORY,
+ "out of memory");
+ return NULL;
+ }
+ cpw->callback = callback;
+
+ g_dbus_connection_call(ns->conn,
+ BLUEZ_SERVICE, path, interface, method, params,
+ NULL, /* reply type */
+ G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT,
+ cpw->cancel, /* cancellable? */
+ bluez_call_async_ready,
+ cpw);
+
+ return cpw;
+}
+
+GVariant *bluez_get_properties(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ GError **error)
+{
+ const char *method = NULL;
+ GVariant *reply = NULL;
+ const char *interface, *interface2 = NULL;
+
+ if (!strcmp(access_type, BLUEZ_AT_DEVICE) ||
+ !strcmp(access_type, BLUEZ_AT_MEDIAPLAYER) ||
+ !strcmp(access_type, BLUEZ_AT_MEDIATRANSPORT) ||
+ !strcmp(access_type, BLUEZ_AT_ADAPTER) ||
+ !strcmp(access_type, BLUEZ_AT_MEDIACONTROL)) {
+ interface = FREEDESKTOP_PROPERTIES;
+ method = "GetAll";
+ } else if (!strcmp(access_type, BLUEZ_AT_OBJECT)) {
+ interface = FREEDESKTOP_OBJECTMANAGER;
+ method = "GetManagedObjects";
+ }
+
+ if (!strcmp(access_type, BLUEZ_AT_DEVICE))
+ interface2 = BLUEZ_DEVICE_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_MEDIAPLAYER))
+ interface2 = BLUEZ_MEDIAPLAYER_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_MEDIATRANSPORT))
+ interface2 = BLUEZ_MEDIATRANSPORT_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_ADAPTER))
+ interface2 = BLUEZ_ADAPTER_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_MEDIACONTROL))
+ interface2 = BLUEZ_MEDIACONTROL_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_OBJECT))
+ interface2 = NULL;
+
+ if (!method) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ "illegal %s argument",
+ access_type);
+ return NULL;
+ }
+
+ reply = g_dbus_connection_call_sync(ns->conn,
+ BLUEZ_SERVICE, path, interface, method,
+ interface2 ? g_variant_new("(s)", interface2) : NULL,
+ NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT,
+ NULL, error);
+ if (!reply) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ "No %s", access_type);
+ return NULL;
+ }
+
+ return reply;
+}
+
+GVariant *bluez_get_property(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ const char *name,
+ GError **error)
+{
+ // NOTE: Only supporting device properties ATM
+ if (!(ns && access_type && path && name) ||
+ strcmp(access_type, BLUEZ_AT_DEVICE)) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ "Bad argument");
+ return NULL;
+ }
+
+ GError *get_error = NULL;
+ GVariant *reply = bluez_get_properties(ns, access_type, path, &get_error);
+ if (get_error || !reply) {
+ if (!get_error)
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_BAD_PROPERTY,
+ "Unexpected error querying properties %s%s%s",
+ access_type,
+ path ? "/" : "",
+ path ? path : "");
+ else if (error)
+ *error = get_error;
+ else
+ g_error_free(get_error);
+ return NULL;
+ }
+
+ GVariantIter *array = NULL;
+ g_variant_get(reply, "(a{sv})", &array);
+ if (!array) {
+ g_variant_unref(reply);
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_BAD_PROPERTY,
+ "Unexpected reply querying property '%s' on %s%s%s",
+ name,
+ access_type,
+ path ? "/" : "",
+ path ? path : "");
+ return NULL;
+ }
+
+ // Look for property by name
+ gchar *key = NULL;
+ GVariant *val = NULL;
+ while (g_variant_iter_loop(array, "{sv}", &key, &val)) {
+ if (!g_strcmp0(name, key))
+ break;
+ }
+ g_free(key);
+ g_variant_unref(reply);
+
+ if (!val)
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_BAD_PROPERTY,
+ "Bad property '%s' on %s%s%s",
+ name,
+ access_type,
+ path ? "/" : "",
+ path ? path : "");
+ return val;
+}
+
+gboolean bluez_set_boolean_property(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ const char *name,
+ gboolean value,
+ GError **error)
+{
+ GVariant *reply, *arg;
+ const char *interface;
+ gchar *propname;
+
+ g_assert(path);
+
+ /* convert to gvariant */
+ arg = g_variant_new_boolean(value);
+
+ /* no variant? error */
+ if (!arg)
+ return FALSE;
+
+ if (!strcmp(access_type, BLUEZ_AT_DEVICE))
+ interface = BLUEZ_DEVICE_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_ADAPTER))
+ interface = BLUEZ_ADAPTER_INTERFACE;
+ else if (!strcmp(access_type, BLUEZ_AT_AGENT))
+ interface = BLUEZ_AGENT_INTERFACE;
+ else
+ return FALSE;
+
+ propname = g_strdup(name);
+ reply = g_dbus_connection_call_sync(ns->conn,
+ BLUEZ_SERVICE, path, FREEDESKTOP_PROPERTIES, "Set",
+ g_variant_new("(ssv)", interface, propname, arg),
+ NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT,
+ NULL, error);
+ g_free(propname);
+
+ if (!reply)
+ return FALSE;
+
+ g_variant_unref(reply);
+
+ return TRUE;
+}
+
+/*
+ * NOTE:
+ * At present the autoconnect behavior is to look for a previously
+ * paired device and stop looking once a connect attempt has been
+ * made. A difference from the binding behavior is that it would
+ * only look for a device once, now the device lookups continue
+ * until a connect attempt is made.
+ *
+ * There is perhaps a case to be made that this functionality should
+ * be left to clients of the library. Or alternatively, rework it
+ * to expose controls such as timeouts and preferred device
+ * specification.
+ */
+gboolean bluez_autoconnect(gpointer data)
+{
+ struct bluez_state *ns = data;
+ gboolean rc = TRUE;
+
+ GVariant *reply = NULL;
+ if (!bluez_adapter_get_devices(NULL, &reply)) {
+ // Reschedule until there's an adapter response
+ return TRUE;
+ }
+
+ GVariantIter *array = NULL;
+ g_variant_get(reply, "a{sv}", &array);
+ const gchar *key = NULL;
+ GVariant *var = NULL;
+ while (g_variant_iter_next(array, "{&sv}", &key, &var)) {
+ GVariantDict *props_dict = g_variant_dict_new(var);
+
+ // Use the device's adapter rather than assuming it
+ gchar *adapter = NULL;
+ if (!(g_variant_dict_lookup(props_dict, "Adapter", "&o", &adapter) && adapter)) {
+ ERROR("could not find device %s adapter", key);
+ continue;
+ }
+
+ gboolean paired = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Paired", "b", &paired) && paired) {
+ gchar *path = g_strconcat("/org/bluez/", adapter, "/", key, NULL);
+ GVariant *connect_reply = bluez_call(ns, "device", path, "Connect", NULL, NULL);
+ g_free(path);
+ if (!connect_reply)
+ continue;
+
+ g_variant_unref(connect_reply);
+
+ // We've initiated connection, stop
+ rc = FALSE;
+ }
+ g_variant_dict_unref(props_dict);
+ g_variant_unref(var);
+ if (!rc)
+ break;
+ }
+ g_variant_iter_free(array);
+ g_variant_unref(reply);
+
+ return rc;
+}
diff --git a/src/bluez-call.h b/src/bluez-call.h
new file mode 100644
index 0000000..211cfea
--- /dev/null
+++ b/src/bluez-call.h
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2018-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUEZ_CALL_H
+#define BLUEZ_CALL_H
+
+#include <glib.h>
+
+#define BLUEZ_DEFAULT_ADAPTER "hci0"
+#define BLUEZ_DEFAULT_PLAYER "player0"
+
+typedef enum {
+ BLUEZ_ERROR_BAD_TECHNOLOGY,
+ BLUEZ_ERROR_BAD_SERVICE,
+ BLUEZ_ERROR_OUT_OF_MEMORY,
+ BLUEZ_ERROR_NO_TECHNOLOGIES,
+ BLUEZ_ERROR_NO_SERVICES,
+ BLUEZ_ERROR_BAD_PROPERTY,
+ BLUEZ_ERROR_UNIMPLEMENTED,
+ BLUEZ_ERROR_UNKNOWN_PROPERTY,
+ BLUEZ_ERROR_UNKNOWN_TECHNOLOGY,
+ BLUEZ_ERROR_UNKNOWN_SERVICE,
+ BLUEZ_ERROR_MISSING_ARGUMENT,
+ BLUEZ_ERROR_ILLEGAL_ARGUMENT,
+ BLUEZ_ERROR_CALL_IN_PROGRESS,
+} NBError;
+
+#define BLUEZ_ERROR (bluez_error_quark())
+
+extern GQuark bluez_error_quark(void);
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1"
+#define BLUEZ_AGENT_INTERFACE BLUEZ_SERVICE ".Agent1"
+#define BLUEZ_AGENTMANAGER_INTERFACE BLUEZ_SERVICE ".AgentManager1"
+#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1"
+#define BLUEZ_MEDIAPLAYER_INTERFACE BLUEZ_SERVICE ".MediaPlayer1"
+#define BLUEZ_MEDIATRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1"
+#define BLUEZ_MEDIACONTROL_INTERFACE BLUEZ_SERVICE ".MediaControl1"
+
+#define BLUEZ_OBJECT_PATH "/"
+#define BLUEZ_PATH "/org/bluez"
+
+#define BLUEZ_ERRMSG(error) \
+ (error ? error->message : "unspecified")
+
+#define FREEDESKTOP_INTROSPECT "org.freedesktop.DBus.Introspectable"
+#define FREEDESKTOP_PROPERTIES "org.freedesktop.DBus.Properties"
+#define FREEDESKTOP_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+#define DBUS_REPLY_TIMEOUT (120 * 1000)
+#define DBUS_REPLY_TIMEOUT_SHORT (10 * 1000)
+
+#define BLUEZ_AT_OBJECT "object"
+#define BLUEZ_AT_ADAPTER "adapter"
+#define BLUEZ_AT_DEVICE "device"
+#define BLUEZ_AT_AGENT "agent"
+#define BLUEZ_AT_AGENTMANAGER "agent-manager"
+#define BLUEZ_AT_MEDIAPLAYER "mediaplayer"
+#define BLUEZ_AT_MEDIATRANSPORT "mediatransport"
+#define BLUEZ_AT_MEDIACONTROL "mediacontrol"
+
+#define BLUEZ_ROOT_PATH(_t) \
+ ({ \
+ call_work_lock(ns); \
+ const char *__t = (_t); \
+ size_t __len = strlen(BLUEZ_PATH) + 1 + \
+ strlen(__t) + 1; \
+ char *__tpath; \
+ __tpath = alloca(__len + 1 + 1); \
+ snprintf(__tpath, __len + 1, \
+ BLUEZ_PATH "/%s", __t); \
+ call_work_unlock(ns); \
+ __tpath; \
+ })
+
+struct bluez_state;
+
+static inline int split_length(const char *path) {
+ gchar **strings = g_strsplit(path, "/", -1);
+ int ret = g_strv_length(strings) ;
+
+ g_strfreev(strings);
+ return ret;
+}
+
+static inline gchar *find_index(const char *path, int idx)
+{
+ gchar **strings = g_strsplit(path, "/", -1);
+ gchar *item = NULL;
+
+ if (g_strv_length(strings) > idx)
+ item = g_strdup(strings[idx]);
+
+ g_strfreev(strings);
+ return item;
+}
+
+static inline gchar *bluez_return_adapter(const char *path)
+{
+ return find_index(path, 3);
+}
+
+static inline gchar *bluez_return_device(const char *path)
+{
+ return find_index(path, 4);
+}
+
+static inline gchar *bluez_return_endpoint(const char *path)
+{
+ gchar **strings = g_strsplit(path, "/", -1);
+ gchar *item = NULL;
+
+ if (g_strv_length(strings) > 6)
+ item = g_strconcat(strings[5], "/", strings[6], NULL);
+
+ g_strfreev(strings);
+ return item;
+}
+
+
+static inline gboolean is_mediaplayer1_interface(const char *path)
+{
+ gchar *data = NULL;
+ gboolean ret;
+
+ // Don't trigger on NowPlaying, Item, etc paths
+ if (split_length(path) != 6)
+ return FALSE;
+
+ // Check for 'playerX' suffix, not always player0
+ data = find_index(path, 5);
+ ret = !strncmp(data, BLUEZ_DEFAULT_PLAYER, sizeof(BLUEZ_DEFAULT_PLAYER) - 1);
+ g_free(data);
+
+ return ret;
+}
+
+static inline gboolean is_mediatransport1_interface(const char *path)
+{
+ gchar *data = NULL;
+ gboolean ret;
+
+ // Don't trigger on NowPlaying, Item, etc paths
+ if (split_length(path) != 6)
+ return FALSE;
+
+ data = find_index(path, 5);
+ ret = g_str_has_prefix(data, "fd");
+ g_free(data);
+
+ return ret;
+}
+
+GVariant *bluez_call(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ GVariant *params,
+ GError **error);
+
+GVariant *bluez_get_properties(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ GError **error);
+
+GVariant *bluez_get_adapter_properties(struct bluez_state *ns,
+ const char *adapter,
+ GError **error);
+
+GVariant *bluez_get_property(struct bluez_state *ns,
+ const char *access_type,
+ const char *path,
+ const char *name,
+ GError **error);
+
+gboolean bluez_set_boolean_property(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *name,
+ gboolean value,
+ GError **error);
+
+gboolean bluez_autoconnect(gpointer data);
+
+struct bluez_pending_work {
+ struct bluez_state *ns;
+ void *user_data;
+ GCancellable *cancel;
+ void (*callback)(void *user_data, GVariant *result, GError **error);
+};
+
+void bluez_cancel_call(struct bluez_state *ns,
+ struct bluez_pending_work *cpw);
+
+struct bluez_pending_work *
+bluez_call_async(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ GVariant *params,
+ GError **error,
+ void (*callback)(void *user_data, GVariant *result, GError **error),
+ void *user_data);
+
+void bluez_decode_call_error(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ GError **error);
+
+#endif /* BLUEZ_CALL_H */
diff --git a/src/call_work.c b/src/call_work.c
new file mode 100644
index 0000000..12f6f4c
--- /dev/null
+++ b/src/call_work.c
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <pthread.h>
+#include <semaphore.h>
+
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include "common.h"
+#include "call_work.h"
+#include "bluez-call.h"
+
+void call_work_lock(struct bluez_state *ns)
+{
+ g_mutex_lock(&ns->cw_mutex);
+}
+
+void call_work_unlock(struct bluez_state *ns)
+{
+ g_mutex_unlock(&ns->cw_mutex);
+}
+
+struct call_work *call_work_lookup_unlocked(
+ struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method)
+{
+ struct call_work *cw;
+ GSList *list;
+
+ /* we can only allow a single pending call */
+ for (list = ns->cw_pending; list; list = g_slist_next(list)) {
+ cw = list->data;
+ if (!g_strcmp0(access_type, cw->access_type) &&
+ !g_strcmp0(type_arg, cw->type_arg) &&
+ !g_strcmp0(method, cw->method))
+ return cw;
+ }
+ return NULL;
+}
+
+struct call_work *call_work_lookup(
+ struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method)
+{
+ struct call_work *cw;
+
+ g_mutex_lock(&ns->cw_mutex);
+ cw = call_work_lookup_unlocked(ns, access_type, type_arg, method);
+ g_mutex_unlock(&ns->cw_mutex);
+
+ return cw;
+}
+
+int call_work_pending_id(
+ struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method)
+{
+ struct call_work *cw;
+ int id = -1;
+
+ g_mutex_lock(&ns->cw_mutex);
+ cw = call_work_lookup_unlocked(ns, access_type, type_arg, method);
+ if (cw)
+ id = cw->id;
+ g_mutex_unlock(&ns->cw_mutex);
+
+ return id;
+}
+
+struct call_work *call_work_lookup_by_id_unlocked(struct bluez_state *ns, int id)
+{
+ struct call_work *cw;
+ GSList *list;
+
+ /* we can only allow a single pending call */
+ for (list = ns->cw_pending; list; list = g_slist_next(list)) {
+ cw = list->data;
+ if (cw->id == id)
+ return cw;
+ }
+ return NULL;
+}
+
+struct call_work *call_work_lookup_by_id(struct bluez_state *ns, int id)
+{
+ struct call_work *cw;
+
+ g_mutex_lock(&ns->cw_mutex);
+ cw = call_work_lookup_by_id_unlocked(ns, id);
+ g_mutex_unlock(&ns->cw_mutex);
+
+ return cw;
+}
+
+struct call_work *call_work_create_unlocked(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ const char *bluez_method,
+ GError **error)
+{
+
+ struct call_work *cw = NULL;
+
+ cw = call_work_lookup_unlocked(ns, access_type, type_arg, method);
+ if (cw) {
+ g_set_error(error, BLUEZ_ERROR, BLUEZ_ERROR_CALL_IN_PROGRESS,
+ "another call in progress (%s/%s/%s)",
+ access_type, type_arg, method);
+ return NULL;
+ }
+
+ /* no other pending; allocate */
+ cw = g_malloc0(sizeof(*cw));
+ cw->ns = ns;
+ do {
+ cw->id = ns->next_cw_id;
+ if (++ns->next_cw_id < 0)
+ ns->next_cw_id = 1;
+ } while (call_work_lookup_by_id_unlocked(ns, cw->id));
+
+ cw->access_type = g_strdup(access_type);
+ cw->type_arg = g_strdup(type_arg);
+ cw->method = g_strdup(method);
+ cw->bluez_method = g_strdup(bluez_method);
+
+ ns->cw_pending = g_slist_prepend(ns->cw_pending, cw);
+
+ return cw;
+}
+
+struct call_work *call_work_create(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ const char *bluez_method,
+ GError **error)
+{
+
+ struct call_work *cw;
+
+ g_mutex_lock(&ns->cw_mutex);
+ cw = call_work_create_unlocked(ns,
+ access_type, type_arg, method, bluez_method,
+ error);
+ g_mutex_unlock(&ns->cw_mutex);
+
+ return cw;
+}
+
+void call_work_destroy_unlocked(struct call_work *cw)
+{
+ struct bluez_state *ns = cw->ns;
+ struct call_work *cw2;
+
+ /* verify that it's something we know about */
+ cw2 = call_work_lookup_by_id_unlocked(ns, cw->id);
+ if (cw2 != cw) {
+ ERROR("Bad call work to destroy");
+ return;
+ }
+
+ /* remove it */
+ ns->cw_pending = g_slist_remove(ns->cw_pending, cw);
+
+ /* agent struct data */
+ g_free(cw->agent_data.device_path);
+ cw->agent_data.device_path = NULL;
+ g_free(cw->access_type);
+ g_free(cw->type_arg);
+ g_free(cw->method);
+ g_free(cw->bluez_method);
+ g_free(cw->agent_data.fixed_pincode);
+ cw->agent_data.fixed_pincode = NULL;
+}
+
+void call_work_destroy(struct call_work *cw)
+{
+ struct bluez_state *ns = cw->ns;
+
+ g_mutex_lock(&ns->cw_mutex);
+ call_work_destroy_unlocked(cw);
+ g_mutex_unlock(&ns->cw_mutex);
+}
diff --git a/src/call_work.h b/src/call_work.h
new file mode 100644
index 0000000..df33583
--- /dev/null
+++ b/src/call_work.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUETOOTH_CALL_H
+#define BLUETOOTH_CALL_H
+
+#include "bluez-agent.h"
+
+struct call_work {
+ struct bluez_state *ns;
+ int id;
+ gchar *access_type;
+ gchar *type_arg;
+ gchar *method;
+ gchar *bluez_method;
+ struct bluez_pending_work *cpw;
+ gpointer request_cb;
+ gpointer request_user_data;
+ struct agent_data agent_data;
+ GDBusMethodInvocation *invocation;
+};
+
+void call_work_lock(struct bluez_state *ns);
+
+void call_work_unlock(struct bluez_state *ns);
+
+struct call_work *call_work_lookup_unlocked(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method);
+
+struct call_work *call_work_create_unlocked(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ const char *bluez_method,
+ GError **error);
+
+struct call_work *call_work_create(struct bluez_state *ns,
+ const char *access_type,
+ const char *type_arg,
+ const char *method,
+ const char *bluez_method,
+ GError **error);
+
+void call_work_destroy(struct call_work *cw);
+
+void call_work_destroy_unlocked(struct call_work *cw);
+
+#endif /* BLUETOOTH_CALL_H */
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..f611d56
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUEZ_COMMON_H
+#define BLUEZ_COMMON_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "bluez-glib.h"
+
+// Marker for exposed API functions
+#define EXPORT __attribute__ ((visibility("default")))
+
+// Global knob for some more expensive verbose debug logging
+#undef BLUEZ_GLIB_DEBUG
+
+struct call_work;
+
+struct bluez_state {
+ GMainLoop *loop;
+ GDBusConnection *conn;
+ guint device_sub;
+ guint autoconnect_sub;
+
+ /* NOTE: single connection allowed for now */
+ /* NOTE: needs locking and a list */
+ GMutex cw_mutex;
+ int next_cw_id;
+ GSList *cw_pending;
+ struct call_work *cw;
+
+ /* agent */
+ GDBusNodeInfo *introspection_data;
+ guint agent_id;
+ guint registration_id;
+ gchar *agent_path;
+ gboolean agent_registered;
+
+ /* mediaplayer */
+ gchar *mediaplayer_path;
+
+ /* default adapter */
+ gchar *default_adapter;
+
+ /* adapter */
+ gchar *adapter;
+};
+
+struct init_data {
+ GCond cond;
+ GMutex mutex;
+ gboolean register_agent;
+ gboolean autoconnect;
+ gboolean init_done;
+ struct bluez_state *ns; // for bluez_register_agent
+ gboolean rc;
+ void (*init_done_cb)(struct init_data *id, gboolean rc);
+ bluez_init_cb_t cb;
+ gpointer user_data;
+};
+
+extern void bluez_log(bluez_log_level_t level, const char *func, const char *format, ...)
+ __attribute__ ((format (printf, 3, 4)));
+
+#define ERROR(format, ...) \
+ bluez_log(BLUEZ_LOG_LEVEL_ERROR, __FUNCTION__, format, ##__VA_ARGS__)
+
+#define WARNING(format, ...) \
+ bluez_log(BLUEZ_LOG_LEVEL_WARNING, __FUNCTION__, format, ##__VA_ARGS__)
+
+#define INFO(format, ...) \
+ bluez_log(BLUEZ_LOG_LEVEL_INFO, __FUNCTION__, format, ##__VA_ARGS__)
+
+#define DEBUG(format, ...) \
+ bluez_log(BLUEZ_LOG_LEVEL_DEBUG, __FUNCTION__, format, ##__VA_ARGS__)
+
+#endif /* BLUEZ_COMMON_H */
diff --git a/src/conf.c b/src/conf.c
new file mode 100644
index 0000000..618c2e7
--- /dev/null
+++ b/src/conf.c
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2018,2020-2021 Konsulko Group
+ * Author: Matt Ranostay <matt.ranostay@konsulko.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <glib.h>
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include "common.h"
+
+// GSM - FIXME - temporary
+static gchar *g_default_adapter = "hci0";
+static gchar *g_default_pincode = "0000";
+
+gchar *get_default_adapter(void)
+{
+#if 0
+ gchar *adapter = NULL;
+ int ret;
+
+ query = json_object_new_object();
+ json_object_object_add(query, "key", json_object_new_string("default_adapter"));
+
+ ret = afb_api_call_sync(api, "persistence", "read", query, &response, NULL, NULL);
+ if (ret < 0)
+ goto out;
+
+ if (json_object_object_get_ex(response, "value", &val))
+ adapter = g_strdup(json_object_get_string(val));
+ json_object_put(response);
+
+out:
+ return adapter;
+#else
+ return g_strdup(g_default_adapter);
+#endif
+
+}
+
+gboolean set_default_adapter(const char *adapter)
+{
+ gboolean rc = TRUE;
+#if 0
+ query = json_object_new_object();
+ json_object_object_add(query, "key", json_object_new_string("default_adapter"));
+ json_object_object_add(query, "value", json_object_new_string(adapter));
+
+ ret = afb_api_call_sync(api, "persistence", "update", query, &response, NULL, NULL);
+ if (ret < 0) {
+ ret = afb_api_call_sync(api, "persistence", "insert", query, &response, NULL, NULL);
+ if (ret < 0)
+ goto out;
+ }
+
+ json_object_put(response);
+
+out:
+#endif
+ return rc;
+}
+
+gchar *get_pincode(void)
+{
+#if 0
+ gchar *pincode = NULL;
+
+ query = json_object_new_object();
+ json_object_object_add(query, "key", json_object_new_string("pincode"));
+
+ afb_api_call_sync(api, "persistence", "read", query, &response, NULL, NULL);
+
+ if (json_object_object_get_ex(response, "value", &val))
+ pincode = g_strdup(json_object_get_string(val));
+ if (!pincode)
+ pincode = g_strdup("1234");
+ json_object_put(response);
+
+ return pincode;
+#else
+ return g_strdup(g_default_pincode);
+#endif
+}
+
+int set_pincode(const char *pincode, char **error)
+{
+ gboolean rc = TRUE;
+#if 0
+ gchar *endptr = NULL;
+ query = json_object_new_object();
+ if (strlen(pincode) > 8 || strlen(pincode) < 4)
+ {
+ *error = "length of pincode must be between 4 and 8 digits";
+ return -1;
+ }
+
+ g_ascii_strtoll(pincode, &endptr, 10);
+ if (*endptr)
+ {
+ *error = "pincode must be digits";
+ return -1;
+ }
+ json_object_object_add(query, "key", json_object_new_string("pincode"));
+ json_object_object_add(query, "value", json_object_new_string(pincode));
+
+ ret = afb_api_call_sync(api, "persistence", "update", query, &response, NULL, NULL);
+ json_object_put(response);
+#endif
+ return rc;
+}
diff --git a/src/conf.h b/src/conf.h
new file mode 100644
index 0000000..9bb1a60
--- /dev/null
+++ b/src/conf.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef BLUEZ_CONF_H
+#define BLUEZ_CONF_H
+
+//#include <stddef.h>
+
+//#define _GNU_SOURCE
+#include <glib.h>
+/*
+#include <stdlib.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+*/
+
+gchar *get_default_adapter(void);
+gboolean set_default_adapter(const char *adapter);
+
+gboolean set_pincode(const char *pincode, char **error);
+gchar *get_pincode(void);
+
+#endif /* BLUEZ_CONF_H */
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..2ce6cb0
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,19 @@
+add_project_arguments('-fvisibility=hidden', language : 'c')
+
+src = ['api.c', 'bluez-agent.c', 'bluez-call.c', 'call_work.c', 'conf.c']
+lib = shared_library('bluez-glib',
+ sources: src,
+ version: '1.0.0',
+ soversion: '0',
+ include_directories: inc,
+ dependencies: [systemd_dep, glib_deps],
+ install: true)
+
+if get_option('build-tester')
+ lib_dep = declare_dependency(link_with: lib)
+ executable('bluez-glib-test',
+ 'test.c',
+ include_directories: inc,
+ dependencies: [systemd_dep, glib_deps, lib_dep])
+endif
+
diff --git a/src/test.c b/src/test.c
new file mode 100644
index 0000000..cd6343b
--- /dev/null
+++ b/src/test.c
@@ -0,0 +1,232 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "bluez-glib.h"
+
+void adapter_event_cb(gchar *adapter, bluez_event_t event, GVariant *properties, gpointer user_data)
+{
+ switch(event) {
+ case BLUEZ_EVENT_ADD:
+ printf("adapter %s add: ", adapter);
+ break;
+ case BLUEZ_EVENT_REMOVE:
+ printf("adapter %s remove: ", adapter);
+ break;
+ case BLUEZ_EVENT_CHANGE:
+ printf("adapter %s change: ", adapter);
+ break;
+ default:
+ break;
+ }
+ gchar *p = g_variant_print(properties, TRUE);
+ printf("%s\n\n", p);
+ g_free(p);
+}
+
+void device_event_cb(gchar *adapter, gchar *device, bluez_event_t event, GVariant *properties, gpointer user_data)
+{
+ switch(event) {
+ case BLUEZ_EVENT_ADD:
+ printf("adapter %s, device %s add: ", adapter, device);
+ break;
+ case BLUEZ_EVENT_REMOVE:
+ printf("adapter %s, device %s remove: ", adapter, device);
+ break;
+ case BLUEZ_EVENT_CHANGE:
+ printf("adapter %s, device %s change: ", adapter, device);
+ break;
+ default:
+ break;
+ }
+ gchar *p = g_variant_print(properties, TRUE);
+ printf("%s\n\n", p);
+ g_free(p);
+}
+
+void media_control_event_cb(gchar *adapter, gchar *device, gchar *endpoint, bluez_event_t event, GVariant *properties, gpointer user_data)
+{
+ switch(event) {
+ case BLUEZ_EVENT_ADD:
+ printf("adapter %s, device %s, endpoint %s add: ", adapter, device, endpoint);
+ break;
+ case BLUEZ_EVENT_REMOVE:
+ printf("adapter %s, device %s, endpoint %s remove: ", adapter, device, endpoint);
+ break;
+ case BLUEZ_EVENT_CHANGE:
+ printf("adapter %s, device %s, endpoint %s change: ", adapter, device, endpoint);
+ break;
+ default:
+ break;
+ }
+ gchar *p = g_variant_print(properties, TRUE);
+ printf("%s\n\n", p);
+ g_free(p);
+}
+
+void media_player_event_cb(gchar *adapter, gchar *device, gchar *player, bluez_event_t event, GVariant *properties, gpointer user_data)
+{
+ switch(event) {
+ case BLUEZ_EVENT_ADD:
+ printf("adapter %s, device %s, player %s add: ", adapter, device, player);
+ break;
+ case BLUEZ_EVENT_REMOVE:
+ printf("adapter %s, device %s, player %s remove: ", adapter, device, player);
+ break;
+ case BLUEZ_EVENT_CHANGE:
+ printf("adapter %s, device %s, player %s change: ", adapter, device, player);
+ break;
+ default:
+ break;
+ }
+ gchar *p = g_variant_print(properties, TRUE);
+ printf("%s\n\n", p);
+ g_free(p);
+}
+
+
+int main(int argc, char *argv[])
+{
+ gboolean rc;
+ char *adapter;
+ GVariant *reply = NULL;
+ GArray *adapters = NULL;
+
+ bluez_add_adapter_event_callback(adapter_event_cb, NULL);
+ bluez_add_device_event_callback(device_event_cb, NULL);
+ bluez_add_media_control_event_callback(media_control_event_cb, NULL);
+ bluez_add_media_player_event_callback(media_player_event_cb, NULL);
+
+ // Should ideally pass callback here and wait for it to report success
+ rc = bluez_init(TRUE, FALSE, NULL, NULL);
+ if (!rc) {
+ printf("bluez_init failed!\n");
+ exit(1);
+ }
+
+ sleep(2);
+
+ GVariant *objects;
+ rc = bluez_get_managed_objects(&objects);
+ if(rc) {
+ printf("managed objects: %s\n", objects ? g_variant_print(objects, TRUE) : "(null)");
+ }
+
+ rc = bluez_get_adapters(&adapters);
+ if(rc && adapters) {
+ printf("# adapters = %d\n", adapters->len);
+ printf("adapters: ");
+ for(int i = 0; i < adapters->len; i++) {
+ gchar *adapter = g_array_index(adapters, gchar*, i);
+ printf("%s ", adapter ? adapter : "(null)");
+ }
+ printf("\n");
+ }
+
+ adapter = bluez_get_default_adapter();
+ if(!adapter) {
+ printf("adapter == NULL!\n");
+ return -1;
+ }
+ printf("default adapter = %s\n", adapter);
+
+ rc = bluez_adapter_get_state(NULL, &reply);
+
+ rc = bluez_adapter_get_devices(NULL, &reply);
+ if(rc) {
+ printf("\nDevices: %s\n\n", reply ? g_variant_print(reply, TRUE) : "(null)");
+ }
+
+ printf("Setting discoverable!\n");
+ rc = bluez_adapter_set_discoverable(adapter, TRUE);
+
+ printf("Starting scan!\n");
+ rc = bluez_adapter_set_discovery(adapter, TRUE);
+
+ rc = bluez_adapter_get_state(NULL, &reply);
+ printf("\n");
+
+ sleep(20);
+
+ rc = bluez_adapter_get_state(NULL, &reply);
+ if(rc) {
+ printf("adapter %s state: %s\n",
+ adapter, reply ? g_variant_print(reply, TRUE) : "(null)");
+ GVariantDict *props_dict = g_variant_dict_new(reply);
+ gboolean powered = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Powered", "b", &powered)) {
+ printf("powered = %d\n", (int) powered);
+ }
+ g_variant_dict_unref(props_dict);
+ g_variant_unref(reply);
+ }
+ printf("\n");
+
+ rc = bluez_adapter_get_devices(NULL, &reply);
+ if(rc) {
+ printf("adapter %s devices: %s\n",
+ adapter, reply ? g_variant_print(reply, TRUE) : "(null)");
+
+ GVariantIter *array = NULL;
+ g_variant_get(reply, "a{sv}", &array);
+ const gchar *key = NULL;
+ GVariant *var = NULL;
+ int i = 0;
+ while (g_variant_iter_next(array, "{&sv}", &key, &var)) {
+ printf("Device %d = %s\n", i++, key);
+
+ GVariantDict *props_dict = g_variant_dict_new(var);
+ if (!props_dict) {
+ g_variant_unref(var);
+ continue;
+ }
+
+ const char *dev_str = key;
+ char *name_str = NULL;
+ char *address_str = NULL;
+ char *paired_str = NULL;
+ char *connected_str = NULL;
+
+ gchar *p = NULL;
+ if (g_variant_dict_lookup(props_dict, "Address", "s", &p)) {
+ address_str = g_strdup(p);
+ g_free(p);
+ }
+
+ p = NULL;
+ if (g_variant_dict_lookup(props_dict, "Name", "s", &p)) {
+ name_str = g_strdup(p);
+ g_free(p);
+ }
+
+ gboolean paired = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Paired", "b", &paired)) {
+ paired_str = paired ? "yes" : "no";
+ }
+
+ gboolean connected = FALSE;
+ if (g_variant_dict_lookup(props_dict, "Connected", "b", &connected)) {
+ connected_str = connected ? "yes" : "no";
+ }
+
+ printf("Device: %s, Address: %s, Name: %s, Paired: %s, Connected: %s\n",
+ dev_str, address_str, name_str, paired_str, connected_str);
+
+ g_variant_unref(var);
+ }
+ g_variant_iter_free(array);
+ }
+ printf("\n");
+
+ printf("Stopping scan!\n");
+ rc = bluez_adapter_set_discovery(adapter, FALSE);
+
+ printf("\n");
+ rc = bluez_adapter_get_state(NULL, &reply);
+ if(rc) {
+ printf("adapter %s state: %s\n",
+ adapter, reply ? g_variant_print(reply, TRUE) : "(null)");
+ }
+
+ return 0;
+}