// SPDX-License-Identifier: GPL-2.0-or-later /* * * BlueZ - Bluetooth protocol stack for Linux * * Copyright (C) 2015 Google Inc. * * */ #ifdef HAVE_CONFIG_H #include #endif #define _GNU_SOURCE #include #include #include #include #include #include "lib/bluetooth.h" #include "lib/mgmt.h" #include "lib/sdp.h" #include "adapter.h" #include "dbus-common.h" #include "error.h" #include "log.h" #include "eir.h" #include "btd.h" #include "src/shared/ad.h" #include "src/shared/mgmt.h" #include "src/shared/queue.h" #include "src/shared/timeout.h" #include "src/shared/util.h" #include "src/shared/crypto.h" #include "advertising.h" #define LE_ADVERTISING_MGR_IFACE "org.bluez.LEAdvertisingManager1" #define LE_ADVERTISEMENT_IFACE "org.bluez.LEAdvertisement1" struct btd_adv_manager { struct btd_adapter *adapter; struct queue *clients; struct mgmt *mgmt; uint16_t mgmt_index; uint8_t max_adv_len; uint8_t max_scan_rsp_len; uint8_t max_ads; uint32_t supported_flags; uint64_t instance_bitmap; bool extended_add_cmds; int8_t min_tx_power; int8_t max_tx_power; }; #define AD_TYPE_BROADCAST 0 #define AD_TYPE_PERIPHERAL 1 /* BLUETOOTH SPECIFICATION Version 5.2 | Vol 4, Part E, page 2585 * defines tx power value indicating no preference */ #define ADV_TX_POWER_NO_PREFERENCE 0x7F struct btd_adv_client { struct btd_adv_manager *manager; char *owner; char *path; char *name; uint16_t appearance; uint16_t duration; uint16_t timeout; uint16_t discoverable_to; unsigned int to_id; unsigned int disc_to_id; unsigned int add_adv_id; GDBusClient *client; GDBusProxy *proxy; DBusMessage *reg; uint8_t type; /* Advertising type */ uint32_t flags; struct bt_ad *data; struct bt_ad *scan; uint8_t instance; uint32_t min_interval; uint32_t max_interval; int8_t tx_power; mgmt_request_func_t refresh_done_func; }; struct dbus_obj_match { const char *owner; const char *path; }; static bool match_client(const void *a, const void *b) { const struct btd_adv_client *client = a; const struct dbus_obj_match *match = b; if (match->owner && g_strcmp0(client->owner, match->owner)) return false; if (match->path && g_strcmp0(client->path, match->path)) return false; return true; } static void client_free(void *data) { struct btd_adv_client *client = data; if (client->to_id > 0) timeout_remove(client->to_id); if (client->disc_to_id > 0) timeout_remove(client->disc_to_id); if (client->client) { g_dbus_client_set_disconnect_watch(client->client, NULL, NULL); g_dbus_client_unref(client->client); } if (client->reg) { DBusMessage *reply; reply = btd_error_failed(client->reg, "Failed to complete registration"); g_dbus_send_message(btd_get_dbus_connection(), reply); dbus_message_unref(client->reg); client->reg = NULL; } if (client->add_adv_id) mgmt_cancel(client->manager->mgmt, client->add_adv_id); if (client->instance) util_clear_uid(&client->manager->instance_bitmap, client->instance); bt_ad_unref(client->data); bt_ad_unref(client->scan); g_dbus_proxy_unref(client->proxy); if (client->owner) g_free(client->owner); if (client->path) g_free(client->path); free(client->name); free(client); } static gboolean client_free_idle_cb(void *data) { client_free(data); return FALSE; } static void client_release(void *data) { struct btd_adv_client *client = data; DBG("Releasing advertisement %s, %s", client->owner, client->path); g_dbus_proxy_method_call(client->proxy, "Release", NULL, NULL, NULL, NULL); } static void client_destroy(void *data) { client_release(data); client_free(data); } static void remove_advertising(struct btd_adv_manager *manager, uint8_t instance) { struct mgmt_cp_remove_advertising cp; if (instance) DBG("instance %u", instance); else DBG("all instances"); cp.instance = instance; mgmt_send(manager->mgmt, MGMT_OP_REMOVE_ADVERTISING, manager->mgmt_index, sizeof(cp), &cp, NULL, NULL, NULL); } static void client_remove(void *data) { struct btd_adv_client *client = data; struct mgmt_cp_remove_advertising cp; g_dbus_client_set_proxy_handlers(client->client, NULL, NULL, NULL, client); g_dbus_client_set_disconnect_watch(client->client, NULL, NULL); cp.instance = client->instance; mgmt_send(client->manager->mgmt, MGMT_OP_REMOVE_ADVERTISING, client->manager->mgmt_index, sizeof(cp), &cp, NULL, NULL, NULL); queue_remove(client->manager->clients, client); g_idle_add(client_free_idle_cb, client); g_dbus_emit_property_changed(btd_get_dbus_connection(), adapter_get_path(client->manager->adapter), LE_ADVERTISING_MGR_IFACE, "SupportedInstances"); g_dbus_emit_property_changed(btd_get_dbus_connection(), adapter_get_path(client->manager->adapter), LE_ADVERTISING_MGR_IFACE, "ActiveInstances"); } static void client_disconnect_cb(DBusConnection *conn, void *user_data) { DBG("Client disconnected"); client_remove(user_data); } static bool parse_type(DBusMessageIter *iter, struct btd_adv_client *client) { const char *msg_type; if (!iter) return true; if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) return false; dbus_message_iter_get_basic(iter, &msg_type); if (!g_strcmp0(msg_type, "broadcast")) { client->type = AD_TYPE_BROADCAST; return true; } if (!g_strcmp0(msg_type, "peripheral")) { client->type = AD_TYPE_PERIPHERAL; return true; } return false; } static bool parse_service_uuids(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter ariter; if (!iter) { bt_ad_clear_service_uuid(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &ariter); bt_ad_clear_service_uuid(client->data); while (dbus_message_iter_get_arg_type(&ariter) == DBUS_TYPE_STRING) { const char *uuid_str; bt_uuid_t uuid; dbus_message_iter_get_basic(&ariter, &uuid_str); DBG("Adding ServiceUUID: %s", uuid_str); if (bt_string_to_uuid(&uuid, uuid_str) < 0) goto fail; if (!bt_ad_add_service_uuid(client->data, &uuid)) goto fail; dbus_message_iter_next(&ariter); } return true; fail: bt_ad_clear_service_uuid(client->data); return false; } static bool parse_solicit_uuids(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter ariter; if (!iter) { bt_ad_clear_solicit_uuid(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &ariter); bt_ad_clear_solicit_uuid(client->data); while (dbus_message_iter_get_arg_type(&ariter) == DBUS_TYPE_STRING) { const char *uuid_str; bt_uuid_t uuid; dbus_message_iter_get_basic(&ariter, &uuid_str); DBG("Adding SolicitUUID: %s", uuid_str); if (bt_string_to_uuid(&uuid, uuid_str) < 0) goto fail; if (!bt_ad_add_solicit_uuid(client->data, &uuid)) goto fail; dbus_message_iter_next(&ariter); } return true; fail: bt_ad_clear_solicit_uuid(client->data); return false; } static bool parse_manufacturer_data(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter entries; if (!iter) { bt_ad_clear_manufacturer_data(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &entries); bt_ad_clear_manufacturer_data(client->data); while (dbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY) { DBusMessageIter value, entry, array; uint16_t manuf_id; uint8_t *manuf_data; int len; dbus_message_iter_recurse(&entries, &entry); dbus_message_iter_get_basic(&entry, &manuf_id); dbus_message_iter_next(&entry); if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) goto fail; dbus_message_iter_recurse(&entry, &value); if (dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_ARRAY) goto fail; dbus_message_iter_recurse(&value, &array); if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_BYTE) goto fail; dbus_message_iter_get_fixed_array(&array, &manuf_data, &len); DBG("Adding ManufacturerData for %04x", manuf_id); if (!bt_ad_add_manufacturer_data(client->data, manuf_id, manuf_data, len)) goto fail; dbus_message_iter_next(&entries); } return true; fail: bt_ad_clear_manufacturer_data(client->data); return false; } static bool parse_service_data(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter entries; if (!iter) { bt_ad_clear_service_data(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &entries); bt_ad_clear_service_data(client->data); while (dbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY) { DBusMessageIter value, entry, array; const char *uuid_str; bt_uuid_t uuid; uint8_t *service_data; int len; dbus_message_iter_recurse(&entries, &entry); dbus_message_iter_get_basic(&entry, &uuid_str); if (bt_string_to_uuid(&uuid, uuid_str) < 0) goto fail; dbus_message_iter_next(&entry); if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) goto fail; dbus_message_iter_recurse(&entry, &value); if (dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_ARRAY) goto fail; dbus_message_iter_recurse(&value, &array); if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_BYTE) goto fail; dbus_message_iter_get_fixed_array(&array, &service_data, &len); DBG("Adding ServiceData for %s", uuid_str); if (!bt_ad_add_service_data(client->data, &uuid, service_data, len)) goto fail; dbus_message_iter_next(&entries); } return true; fail: bt_ad_clear_service_data(client->data); return false; } static bool set_rsi(struct btd_adv_client *client) { struct bt_crypto *crypto; uint8_t zero[16] = {}; struct bt_ad_data rsi = { .type = BT_AD_CSIP_RSI }; uint8_t data[6]; bool ret; /* Check if a valid SIRK has been set */ if (!memcmp(btd_opts.csis.sirk, zero, sizeof(zero))) return false; /* Check if RSI needs to be set or data already contains RSI data */ if (!client || bt_ad_has_data(client->data, &rsi)) return true; crypto = bt_crypto_new(); if (!crypto) return false; ret = bt_crypto_random_bytes(crypto, data + 3, sizeof(data) - 3); if (!ret) goto done; ret = bt_crypto_sih(crypto, btd_opts.csis.sirk, data + 3, data); if (!ret) goto done; ret = bt_ad_add_data(client->data, BT_AD_CSIP_RSI, data, sizeof(data)); done: bt_crypto_unref(crypto); return ret; } static struct adv_include { uint8_t flag; const char *name; bool (*set)(struct btd_adv_client *client); } includes[] = { { MGMT_ADV_FLAG_TX_POWER, "tx-power" }, { MGMT_ADV_FLAG_APPEARANCE, "appearance" }, { MGMT_ADV_FLAG_LOCAL_NAME, "local-name" }, { 0 , "rsi", set_rsi }, { }, }; static bool parse_includes(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter entries; if (!iter) { client->flags = 0; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &entries); /* Reset flags before parsing */ client->flags = 0; while (dbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_STRING) { const char *str; struct adv_include *inc; dbus_message_iter_get_basic(&entries, &str); for (inc = includes; inc && inc->name; inc++) { if (strcmp(str, inc->name)) continue; if (inc->set && inc->set(client)) { DBG("Including Feature: %s", str); continue; } if (!(client->manager->supported_flags & inc->flag)) continue; DBG("Including Feature: %s", str); client->flags |= inc->flag; } dbus_message_iter_next(&entries); } return true; } static bool parse_local_name(DBusMessageIter *iter, struct btd_adv_client *client) { const char *name; if (!iter) { free(client->name); client->name = NULL; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) return false; if (client->flags & MGMT_ADV_FLAG_LOCAL_NAME) { error("Local name already included"); return false; } dbus_message_iter_get_basic(iter, &name); free(client->name); /* Treat empty string the same as omitting since there is no point on * adding a empty name as AD data as it just take space that could be * used for something else. */ if (name[0] != '\0') client->name = strdup(name); else client->name = NULL; return true; } static bool parse_appearance(DBusMessageIter *iter, struct btd_adv_client *client) { if (!iter) { client->appearance = 0; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT16) return false; if (client->flags & MGMT_ADV_FLAG_APPEARANCE) { error("Appearance already included"); return false; } dbus_message_iter_get_basic(iter, &client->appearance); return true; } static bool parse_duration(DBusMessageIter *iter, struct btd_adv_client *client) { if (!iter) { client->duration = 0; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT16) return false; dbus_message_iter_get_basic(iter, &client->duration); return true; } static bool client_timeout(void *user_data) { struct btd_adv_client *client = user_data; DBG(""); client->to_id = 0; client_release(client); client_remove(client); return FALSE; } static bool parse_timeout(DBusMessageIter *iter, struct btd_adv_client *client) { if (!iter) { client->timeout = 0; timeout_remove(client->to_id); client->to_id = 0; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT16) return false; dbus_message_iter_get_basic(iter, &client->timeout); if (client->to_id) timeout_remove(client->to_id); if (client->timeout > 0) client->to_id = timeout_add_seconds(client->timeout, client_timeout, client, NULL); return true; } static bool parse_data(DBusMessageIter *iter, struct btd_adv_client *client) { DBusMessageIter entries; if (!iter) { bt_ad_clear_data(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) return false; dbus_message_iter_recurse(iter, &entries); bt_ad_clear_data(client->data); while (dbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY) { DBusMessageIter value, entry, array; uint8_t type; uint8_t *data; int len; dbus_message_iter_recurse(&entries, &entry); dbus_message_iter_get_basic(&entry, &type); dbus_message_iter_next(&entry); if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) goto fail; dbus_message_iter_recurse(&entry, &value); if (dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_ARRAY) goto fail; dbus_message_iter_recurse(&value, &array); if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_BYTE) goto fail; dbus_message_iter_get_fixed_array(&array, &data, &len); DBG("Adding Data for type 0x%02x len %u", type, len); if (!bt_ad_add_data(client->data, type, data, len)) goto fail; dbus_message_iter_next(&entries); } return true; fail: bt_ad_clear_data(client->data); return false; } static bool set_flags(struct btd_adv_client *client, uint8_t flags) { if (!flags) { bt_ad_clear_flags(client->data); return true; } /* Set BR/EDR Not Supported for LE only */ if (!btd_adapter_get_bredr(client->manager->adapter)) flags |= BT_AD_FLAG_NO_BREDR; /* Set BR/EDR Not Supported if adapter is not discoverable but the * instance is. */ if ((flags & (BT_AD_FLAG_GENERAL | BT_AD_FLAG_LIMITED)) && !btd_adapter_get_discoverable(client->manager->adapter)) flags |= BT_AD_FLAG_NO_BREDR; if (!bt_ad_add_flags(client->data, &flags, 1)) return false; return true; } static bool parse_discoverable(DBusMessageIter *iter, struct btd_adv_client *client) { uint8_t flags; dbus_bool_t discoverable; if (!iter) { bt_ad_clear_flags(client->data); return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_BOOLEAN) return false; dbus_message_iter_get_basic(iter, &discoverable); if (discoverable) flags = BT_AD_FLAG_GENERAL; else flags = 0x00; if (!set_flags(client , flags)) goto fail; DBG("Adding Flags 0x%02x", flags); return true; fail: bt_ad_clear_flags(client->data); return false; } static size_t calc_max_adv_len(struct btd_adv_client *client, uint32_t flags) { size_t max = client->manager->max_adv_len; /* * Flags which reduce the amount of space available for advertising. * See doc/mgmt-api.txt */ if (flags & MGMT_ADV_FLAG_TX_POWER) max -= 3; if (flags & (MGMT_ADV_FLAG_DISCOV | MGMT_ADV_FLAG_LIMITED_DISCOV | MGMT_ADV_FLAG_MANAGED_FLAGS)) max -= 3; if (flags & MGMT_ADV_FLAG_APPEARANCE) max -= 4; return max; } static uint8_t *generate_adv_data(struct btd_adv_client *client, uint32_t *flags, size_t *len) { if ((*flags & MGMT_ADV_FLAG_APPEARANCE) || client->appearance != UINT16_MAX) { uint16_t appearance; appearance = client->appearance; if (appearance == UINT16_MAX) /* TODO: Get the appearance from the adaptor once * supported. */ appearance = 0x000; bt_ad_add_appearance(client->data, appearance); } /* Scan response shall not be used when connectable and setting a * secondary PHY since that would end up using EA types instead of * legacy which doesn't support being connectable and scannable * simultaneously. */ if ((*flags & MGMT_ADV_FLAG_CONNECTABLE) && (*flags & MGMT_ADV_FLAG_SEC_MASK) && client->name) { *flags &= ~MGMT_ADV_FLAG_LOCAL_NAME; bt_ad_add_name(client->data, client->name); } return bt_ad_generate(client->data, len); } static uint8_t *generate_scan_rsp(struct btd_adv_client *client, uint32_t *flags, size_t *len) { if (!client->name) { *len = 0; return NULL; } *flags &= ~MGMT_ADV_FLAG_LOCAL_NAME; bt_ad_add_name(client->scan, client->name); return bt_ad_generate(client->scan, len); } static bool adv_client_has_scan_response(struct btd_adv_client *client, uint32_t flags) { /* The local name isn't added into the bt_ad structure until * generate_scan_rsp is called, so we must check these conditions as * well. */ if (!(flags & MGMT_ADV_FLAG_LOCAL_NAME) && !client->name && bt_ad_is_empty(client->scan)) { return false; } /* Scan response shall not be used when connectable and setting a * secondary PHY since that would end up using EA types instead of * legacy which doesn't support being connectable and scannable * simultaneously. */ if (flags & MGMT_ADV_FLAG_CONNECTABLE && flags & MGMT_ADV_FLAG_SEC_MASK) return false; return true; } static int get_adv_flags(struct btd_adv_client *client) { uint32_t flags = 0; if (client->type == AD_TYPE_PERIPHERAL) { flags = MGMT_ADV_FLAG_CONNECTABLE; if (btd_adapter_get_discoverable(client->manager->adapter) && !(bt_ad_has_flags(client->data))) flags |= MGMT_ADV_FLAG_DISCOV; } flags |= client->flags; return flags; } static int refresh_legacy_adv(struct btd_adv_client *client, mgmt_request_func_t func, unsigned int *mgmt_id) { struct mgmt_cp_add_advertising *cp; uint8_t param_len; uint8_t *adv_data; size_t adv_data_len; uint8_t *scan_rsp; size_t scan_rsp_len = -1; uint32_t flags = 0; unsigned int mgmt_ret; DBG("Refreshing advertisement: %s", client->path); flags = get_adv_flags(client); adv_data = generate_adv_data(client, &flags, &adv_data_len); if (!adv_data || (adv_data_len > calc_max_adv_len(client, flags))) { error("Advertising data too long or couldn't be generated."); return -EINVAL; } scan_rsp = generate_scan_rsp(client, &flags, &scan_rsp_len); if (!scan_rsp && scan_rsp_len) { error("Scan data couldn't be generated."); free(adv_data); return -EINVAL; } param_len = sizeof(struct mgmt_cp_add_advertising) + adv_data_len + scan_rsp_len; cp = malloc0(param_len); if (!cp) { error("Couldn't allocate for MGMT!"); free(adv_data); free(scan_rsp); return -ENOMEM; } cp->flags = htobl(flags); cp->instance = client->instance; cp->duration = client->duration; cp->adv_data_len = adv_data_len; cp->scan_rsp_len = scan_rsp_len; memcpy(cp->data, adv_data, adv_data_len); memcpy(cp->data + adv_data_len, scan_rsp, scan_rsp_len); free(adv_data); free(scan_rsp); mgmt_ret = mgmt_send(client->manager->mgmt, MGMT_OP_ADD_ADVERTISING, client->manager->mgmt_index, param_len, cp, func, client, NULL); if (!mgmt_ret) { error("Failed to add Advertising Data"); free(cp); return -EINVAL; } if (mgmt_id) *mgmt_id = mgmt_ret; free(cp); return 0; } static void add_adv_params_callback(uint8_t status, uint16_t length, const void *param, void *user_data); static int refresh_extended_adv(struct btd_adv_client *client, mgmt_request_func_t func, unsigned int *mgmt_id) { struct mgmt_cp_add_ext_adv_params cp; uint32_t flags = 0; unsigned int mgmt_ret = 0; DBG("Refreshing advertisement parameters: %s", client->path); flags = get_adv_flags(client); memset(&cp, 0, sizeof(cp)); cp.instance = client->instance; /* Not all advertising instances will use all possible parameters. The * included_params bit field tells the kernel which parameters are * relevant, and sensible defaults will be used for the rest */ if (client->duration) { cp.duration = client->duration; flags |= MGMT_ADV_PARAM_DURATION; } if (client->min_interval && client->max_interval) { cp.min_interval = client->min_interval; cp.max_interval = client->max_interval; flags |= MGMT_ADV_PARAM_INTERVALS; } if (client->tx_power != ADV_TX_POWER_NO_PREFERENCE) { cp.tx_power = client->tx_power; flags |= MGMT_ADV_PARAM_TX_POWER; } /* Indicate that this instance will be configured as scannable */ if (adv_client_has_scan_response(client, flags) && client->manager->supported_flags & MGMT_ADV_PARAM_SCAN_RSP) { flags |= MGMT_ADV_PARAM_SCAN_RSP; } cp.flags = cpu_to_le32(flags); mgmt_ret = mgmt_send(client->manager->mgmt, MGMT_OP_ADD_EXT_ADV_PARAMS, client->manager->mgmt_index, sizeof(cp), &cp, add_adv_params_callback, client, NULL); if (!mgmt_ret) { error("Failed to request extended advertising parameters"); return -EINVAL; } /* Store callback, called after we set advertising data */ client->refresh_done_func = func; if (mgmt_id) *mgmt_id = mgmt_ret; return 0; } static int refresh_advertisement(struct btd_adv_client *client, mgmt_request_func_t func, unsigned int *mgmt_id) { if (client->manager->extended_add_cmds) return refresh_extended_adv(client, func, mgmt_id); return refresh_legacy_adv(client, func, mgmt_id); } static bool client_discoverable_timeout(void *user_data) { struct btd_adv_client *client = user_data; DBG(""); client->disc_to_id = 0; bt_ad_clear_flags(client->data); refresh_advertisement(client, NULL, NULL); return FALSE; } static bool parse_discoverable_timeout(DBusMessageIter *iter, struct btd_adv_client *client) { if (!iter) { client->discoverable_to = 0; timeout_remove(client->disc_to_id); client->disc_to_id = 0; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT16) return false; dbus_message_iter_get_basic(iter, &client->discoverable_to); if (client->disc_to_id) timeout_remove(client->disc_to_id); client->disc_to_id = timeout_add_seconds(client->discoverable_to, client_discoverable_timeout, client, NULL); return true; } static struct adv_secondary { int flag; const char *name; } secondary[] = { { MGMT_ADV_FLAG_SEC_1M, "1M" }, { MGMT_ADV_FLAG_SEC_2M, "2M" }, { MGMT_ADV_FLAG_SEC_CODED, "Coded" }, { }, }; static bool parse_secondary(DBusMessageIter *iter, struct btd_adv_client *client) { const char *str; struct adv_secondary *sec; if (!iter) { /* Reset secondary channels */ client->flags &= ~MGMT_ADV_FLAG_SEC_MASK; return true; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) return false; /* Reset secondary channels before parsing */ client->flags &= ~MGMT_ADV_FLAG_SEC_MASK; dbus_message_iter_get_basic(iter, &str); for (sec = secondary; sec->name; sec++) { if (strcmp(str, sec->name)) continue; if (!(client->manager->supported_flags & sec->flag)) return false; DBG("Secondary Channel: %s", str); client->flags |= sec->flag; return true; } return false; } static bool parse_min_interval(DBusMessageIter *iter, struct btd_adv_client *client) { uint32_t min_interval_ms; /* Only consider this property if experimental setting is applied */ if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL)) return true; if (!iter) { client->min_interval = 0; return false; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT32) return false; dbus_message_iter_get_basic(iter, &min_interval_ms); /* Convert ms to jiffies to be used in adv request */ client->min_interval = min_interval_ms / 0.625; /* BLUETOOTH SPECIFICATION Version 5.2 | Vol 4, Part E, page 2584 * defines acceptable interval range */ if (client->min_interval < 0x20 || client->min_interval > 0xFFFFFF) { client->min_interval = 0; return false; } return true; } static bool parse_max_interval(DBusMessageIter *iter, struct btd_adv_client *client) { uint32_t max_interval_ms; /* Only consider this property if experimental setting is applied */ if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL)) return true; if (!iter) { client->max_interval = 0; return false; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT32) return false; dbus_message_iter_get_basic(iter, &max_interval_ms); /* Convert ms to jiffies to be used in adv request */ client->max_interval = max_interval_ms / 0.625; /* BLUETOOTH SPECIFICATION Version 5.2 | Vol 4, Part E, page 2584 * defines acceptable interval range */ if (client->max_interval < 0x20 || client->max_interval > 0xFFFFFF) { client->max_interval = 0; return false; } return true; } static bool parse_tx_power(DBusMessageIter *iter, struct btd_adv_client *client) { int16_t val; /* Only consider this property if experimental setting is applied */ if (!(g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL)) return true; if (!iter) { client->tx_power = ADV_TX_POWER_NO_PREFERENCE; return false; } if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_INT16) return false; dbus_message_iter_get_basic(iter, &val); /* BLUETOOTH SPECIFICATION Version 5.2 | Vol 4, Part E, page 2585 * defines acceptable tx power range */ if (val < -127 || val > 20) return false; client->tx_power = val; return true; } static struct adv_parser { const char *name; bool (*func)(DBusMessageIter *iter, struct btd_adv_client *client); } parsers[] = { { "Type", parse_type }, { "ServiceUUIDs", parse_service_uuids }, { "SolicitUUIDs", parse_solicit_uuids }, { "ManufacturerData", parse_manufacturer_data }, { "ServiceData", parse_service_data }, { "Includes", parse_includes }, { "LocalName", parse_local_name }, { "Appearance", parse_appearance }, { "Duration", parse_duration }, { "Timeout", parse_timeout }, { "Data", parse_data }, { "Discoverable", parse_discoverable }, { "DiscoverableTimeout", parse_discoverable_timeout }, { "SecondaryChannel", parse_secondary }, { "MinInterval", parse_min_interval }, { "MaxInterval", parse_max_interval }, { "TxPower", parse_tx_power }, { }, }; static void properties_changed(GDBusProxy *proxy, const char *name, DBusMessageIter *iter, void *user_data) { struct btd_adv_client *client = user_data; struct adv_parser *parser; for (parser = parsers; parser && parser->name; parser++) { if (strcmp(parser->name, name)) continue; if (parser->func(iter, client)) { refresh_advertisement(client, NULL, NULL); break; } } } static void add_client_complete(struct btd_adv_client *client, uint8_t status) { DBusMessage *reply; if (status) { error("Failed to add advertisement: %s (0x%02x)", mgmt_errstr(status), status); reply = btd_error_failed(client->reg, "Failed to register advertisement"); queue_remove(client->manager->clients, client); g_idle_add(client_free_idle_cb, client); } else reply = dbus_message_new_method_return(client->reg); g_dbus_send_message(btd_get_dbus_connection(), reply); dbus_message_unref(client->reg); client->reg = NULL; } static void add_adv_callback(uint8_t status, uint16_t length, const void *param, void *user_data) { struct btd_adv_client *client = user_data; const struct mgmt_rp_add_advertising *rp = param; client->add_adv_id = 0; if (status) goto done; if (!param || length < sizeof(*rp)) { status = MGMT_STATUS_FAILED; goto done; } client->instance = rp->instance; g_dbus_client_set_disconnect_watch(client->client, client_disconnect_cb, client); DBG("Advertisement registered: %s", client->path); g_dbus_emit_property_changed(btd_get_dbus_connection(), adapter_get_path(client->manager->adapter), LE_ADVERTISING_MGR_IFACE, "SupportedInstances"); g_dbus_emit_property_changed(btd_get_dbus_connection(), adapter_get_path(client->manager->adapter), LE_ADVERTISING_MGR_IFACE, "ActiveInstances"); g_dbus_proxy_set_property_watch(client->proxy, properties_changed, client); done: add_client_complete(client, status); } static void add_adv_params_callback(uint8_t status, uint16_t length, const void *param, void *user_data) { struct btd_adv_client *client = user_data; const struct mgmt_rp_add_ext_adv_params *rp = param; struct mgmt_cp_add_ext_adv_data *cp = NULL; uint8_t param_len; uint8_t *adv_data = NULL; size_t adv_data_len; uint8_t *scan_rsp = NULL; size_t scan_rsp_len = 0; uint32_t flags = 0; unsigned int mgmt_ret; dbus_int16_t tx_power; if (status) goto fail; if (!param || length < sizeof(*rp)) { status = MGMT_STATUS_FAILED; goto fail; } DBG("Refreshing advertisement data: %s", client->path); /* Update tx power held by client */ tx_power = rp->tx_power; if (tx_power != ADV_TX_POWER_NO_PREFERENCE) g_dbus_proxy_set_property_basic(client->proxy, "TxPower", DBUS_TYPE_INT16, &tx_power, NULL, NULL, NULL); client->instance = rp->instance; flags = get_adv_flags(client); adv_data = generate_adv_data(client, &flags, &adv_data_len); if (!adv_data || (adv_data_len > rp->max_adv_data_len)) { error("Advertising data too long or couldn't be generated."); goto fail; } if (adv_client_has_scan_response(client, flags)) { scan_rsp = generate_scan_rsp(client, &flags, &scan_rsp_len); if ((!scan_rsp && scan_rsp_len) || scan_rsp_len > rp->max_scan_rsp_len) { error("Scan data couldn't be generated."); goto fail; } } param_len = sizeof(struct mgmt_cp_add_advertising) + adv_data_len + scan_rsp_len; cp = malloc0(param_len); if (!cp) { error("Couldn't allocate for MGMT!"); goto fail; } cp->instance = client->instance; cp->adv_data_len = adv_data_len; cp->scan_rsp_len = scan_rsp_len; memcpy(cp->data, adv_data, adv_data_len); if (scan_rsp) memcpy(cp->data + adv_data_len, scan_rsp, scan_rsp_len); free(adv_data); free(scan_rsp); adv_data = NULL; scan_rsp = NULL; /* Submit request to update instance data */ mgmt_ret = mgmt_send(client->manager->mgmt, MGMT_OP_ADD_EXT_ADV_DATA, client->manager->mgmt_index, param_len, cp, client->refresh_done_func, client, NULL); /* Clear the callback */ client->refresh_done_func = NULL; if (!mgmt_ret) { error("Failed to add Advertising Data"); goto fail; } if (client->add_adv_id) client->add_adv_id = mgmt_ret; free(cp); cp = NULL; return; fail: if (adv_data) free(adv_data); if (scan_rsp) free(scan_rsp); if (cp) free(cp); if (!status) status = -EINVAL; /* Failure for any reason ends this advertising request */ add_client_complete(client, status); } static DBusMessage *parse_advertisement(struct btd_adv_client *client) { struct adv_parser *parser; int err; for (parser = parsers; parser && parser->name; parser++) { DBusMessageIter iter; if (!g_dbus_proxy_get_property(client->proxy, parser->name, &iter)) continue; if (!parser->func(&iter, client)) { error("Error parsing %s property", parser->name); goto fail; } } if (bt_ad_has_flags(client->data)) { /* BLUETOOTH SPECIFICATION Version 5.0 | Vol 3, Part C * page 2042: * A device in the broadcast mode shall not set the * ‘LE General Discoverable Mode’ flag or the * ‘LE Limited Discoverable Mode’ flag in the Flags AD Type * as defined in [Core Specification Supplement], Part A, * Section 1.3. */ if (client->type == AD_TYPE_BROADCAST) { error("Broadcast cannot set flags"); goto fail; } /* Set Limited Discoverable if DiscoverableTimeout is set */ if (client->disc_to_id && !set_flags(client, BT_AD_FLAG_LIMITED)) { error("Failed to set Limited Discoverable Flag"); goto fail; } } else if (client->disc_to_id) { /* Ignore DiscoverableTimeout if not discoverable */ timeout_remove(client->disc_to_id); client->disc_to_id = 0; client->discoverable_to = 0; } if (client->timeout && client->timeout < client->discoverable_to) { /* DiscoverableTimeout must not be bigger than Timeout */ error("DiscoverableTimeout > Timeout"); goto fail; } if (client->min_interval > client->max_interval) { /* Min interval must not be bigger than max interval */ error("MinInterval must be less than MaxInterval (%u > %u)", client->min_interval, client->max_interval); goto fail; } err = refresh_advertisement(client, add_adv_callback, &client->add_adv_id); if (!err) return NULL; fail: return btd_error_failed(client->reg, "Failed to parse advertisement."); } static void client_proxy_added(GDBusProxy *proxy, void *data) { struct btd_adv_client *client = data; DBusMessage *reply; const char *interface; interface = g_dbus_proxy_get_interface(proxy); if (g_str_equal(interface, LE_ADVERTISEMENT_IFACE) == FALSE) return; reply = parse_advertisement(client); if (!reply) return; /* Failed to publish for some reason, remove. */ queue_remove(client->manager->clients, client); g_idle_add(client_free_idle_cb, client); g_dbus_send_message(btd_get_dbus_connection(), reply); dbus_message_unref(client->reg); client->reg = NULL; } static struct btd_adv_client *client_create(struct btd_adv_manager *manager, DBusConnection *conn, DBusMessage *msg, const char *path) { struct btd_adv_client *client; const char *sender = dbus_message_get_sender(msg); if (!path || !g_str_has_prefix(path, "/")) return NULL; client = new0(struct btd_adv_client, 1); client->client = g_dbus_client_new_full(conn, sender, path, path); if (!client->client) goto fail; client->owner = g_strdup(sender); if (!client->owner) goto fail; client->path = g_strdup(path); if (!client->path) goto fail; DBG("Adding proxy for %s", path); client->proxy = g_dbus_proxy_new(client->client, path, LE_ADVERTISEMENT_IFACE); if (!client->proxy) goto fail; g_dbus_client_set_proxy_handlers(client->client, client_proxy_added, NULL, NULL, client); client->data = bt_ad_new(); if (!client->data) goto fail; client->scan = bt_ad_new(); if (!client->scan) goto fail; client->manager = manager; client->appearance = UINT16_MAX; client->tx_power = ADV_TX_POWER_NO_PREFERENCE; client->min_interval = 0; client->max_interval = 0; client->refresh_done_func = NULL; return client; fail: client_free(client); return NULL; } static DBusMessage *register_advertisement(DBusConnection *conn, DBusMessage *msg, void *user_data) { struct btd_adv_manager *manager = user_data; DBusMessageIter args; struct btd_adv_client *client; struct dbus_obj_match match; DBG("RegisterAdvertisement"); if (!dbus_message_iter_init(msg, &args)) return btd_error_invalid_args(msg); if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_OBJECT_PATH) return btd_error_invalid_args(msg); dbus_message_iter_get_basic(&args, &match.path); match.owner = dbus_message_get_sender(msg); if (queue_find(manager->clients, match_client, &match)) return btd_error_already_exists(msg); dbus_message_iter_next(&args); if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_ARRAY) return btd_error_invalid_args(msg); client = client_create(manager, conn, msg, match.path); if (!client) return btd_error_failed(msg, "Failed to register advertisement"); client->instance = util_get_uid(&manager->instance_bitmap, manager->max_ads); if (!client->instance) { client_free(client); return btd_error_not_permitted(msg, "Maximum advertisements reached"); } DBG("Registered advertisement at path %s", match.path); client->reg = dbus_message_ref(msg); queue_push_tail(manager->clients, client); return NULL; } static DBusMessage *unregister_advertisement(DBusConnection *conn, DBusMessage *msg, void *user_data) { struct btd_adv_manager *manager = user_data; DBusMessageIter args; struct btd_adv_client *client; struct dbus_obj_match match; DBG("UnregisterAdvertisement"); if (!dbus_message_iter_init(msg, &args)) return btd_error_invalid_args(msg); if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_OBJECT_PATH) return btd_error_invalid_args(msg); dbus_message_iter_get_basic(&args, &match.path); match.owner = dbus_message_get_sender(msg); client = queue_find(manager->clients, match_client, &match); if (!client) return btd_error_does_not_exist(msg); client_remove(client); return dbus_message_new_method_return(msg); } static gboolean get_instances(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; uint8_t instances; instances = manager->max_ads - queue_length(manager->clients); dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, &instances); return TRUE; } static gboolean get_active_instances(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; uint8_t instances; instances = queue_length(manager->clients); dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, &instances); return TRUE; } static void append_include(struct btd_adv_manager *manager, DBusMessageIter *iter) { struct adv_include *inc; for (inc = includes; inc && inc->name; inc++) { if ((inc->set && inc->set(NULL)) || (manager->supported_flags & inc->flag)) dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &inc->name); } } static gboolean get_supported_includes(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; DBusMessageIter entry; dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &entry); append_include(manager, &entry); dbus_message_iter_close_container(iter, &entry); return TRUE; } static void append_secondary(struct btd_adv_manager *manager, DBusMessageIter *iter) { struct adv_secondary *sec; for (sec = secondary; sec && sec->name; sec++) { if (manager->supported_flags & sec->flag) dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &sec->name); } } static gboolean secondary_exists(const GDBusPropertyTable *property, void *data) { struct btd_adv_manager *manager = data; /* 1M PHY shall always be supported if ext_adv is supported */ return manager->supported_flags & MGMT_ADV_FLAG_SEC_1M; } static gboolean get_supported_secondary(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; DBusMessageIter entry; dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &entry); append_secondary(manager, &entry); dbus_message_iter_close_container(iter, &entry); return TRUE; } static struct adv_feature { int flag; const char *name; } features[] = { { MGMT_ADV_FLAG_CAN_SET_TX_POWER, "CanSetTxPower" }, { MGMT_ADV_FLAG_HW_OFFLOAD, "HardwareOffload" }, { }, }; static void append_features(struct btd_adv_manager *manager, DBusMessageIter *iter) { struct adv_feature *feat; for (feat = features; feat->name; feat++) { if (manager->supported_flags & feat->flag) dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &feat->name); } } static gboolean get_supported_features(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; DBusMessageIter entry; dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &entry); append_features(manager, &entry); dbus_message_iter_close_container(iter, &entry); return TRUE; } static gboolean get_supported_cap(const GDBusPropertyTable *property, DBusMessageIter *iter, void *data) { struct btd_adv_manager *manager = data; DBusMessageIter dict; int16_t min_tx_power = manager->min_tx_power; int16_t max_tx_power = manager->max_tx_power; dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); if (min_tx_power != ADV_TX_POWER_NO_PREFERENCE) dict_append_entry(&dict, "MinTxPower", DBUS_TYPE_INT16, &min_tx_power); if (max_tx_power != ADV_TX_POWER_NO_PREFERENCE) dict_append_entry(&dict, "MaxTxPower", DBUS_TYPE_INT16, &max_tx_power); dict_append_entry(&dict, "MaxAdvLen", DBUS_TYPE_BYTE, &manager->max_adv_len); dict_append_entry(&dict, "MaxScnRspLen", DBUS_TYPE_BYTE, &manager->max_scan_rsp_len); dbus_message_iter_close_container(iter, &dict); return TRUE; } static const GDBusPropertyTable properties[] = { { "ActiveInstances", "y", get_active_instances, NULL, NULL }, { "SupportedInstances", "y", get_instances, NULL, NULL }, { "SupportedIncludes", "as", get_supported_includes, NULL, NULL }, { "SupportedSecondaryChannels", "as", get_supported_secondary, NULL, secondary_exists }, { "SupportedFeatures", "as", get_supported_features, NULL, NULL, G_DBUS_PROPERTY_FLAG_EXPERIMENTAL}, { "SupportedCapabilities", "a{sv}", get_supported_cap, NULL, NULL, G_DBUS_PROPERTY_FLAG_EXPERIMENTAL}, { } }; static const GDBusMethodTable methods[] = { { GDBUS_ASYNC_METHOD("RegisterAdvertisement", GDBUS_ARGS({ "advertisement", "o" }, { "options", "a{sv}" }), NULL, register_advertisement) }, { GDBUS_ASYNC_METHOD("UnregisterAdvertisement", GDBUS_ARGS({ "service", "o" }), NULL, unregister_advertisement) }, { } }; static void manager_destroy(void *user_data) { struct btd_adv_manager *manager = user_data; queue_destroy(manager->clients, client_destroy); mgmt_unref(manager->mgmt); free(manager); } static void read_adv_features_callback(uint8_t status, uint16_t length, const void *param, void *user_data) { struct btd_adv_manager *manager = user_data; const struct mgmt_rp_read_adv_features *feat = param; if (status || !param) { error("Failed to read advertising features: %s (0x%02x)", mgmt_errstr(status), status); return; } if (length < sizeof(*feat)) { error("Wrong size of read adv features response"); return; } manager->max_adv_len = feat->max_adv_data_len; manager->max_scan_rsp_len = feat->max_scan_rsp_len; manager->max_ads = feat->max_instances; manager->supported_flags |= feat->supported_flags; /* Registering interface after querying properties */ if (!g_dbus_register_interface(btd_get_dbus_connection(), adapter_get_path(manager->adapter), LE_ADVERTISING_MGR_IFACE, methods, NULL, properties, manager, NULL)) error("Failed to register " LE_ADVERTISING_MGR_IFACE); if (manager->max_ads == 0) return; /* Reset existing instances */ if (feat->num_instances) remove_advertising(manager, 0); } static void read_controller_cap_complete(uint8_t status, uint16_t length, const void *param, void *user_data) { struct btd_adv_manager *manager = user_data; const struct mgmt_rp_read_controller_cap *rp = param; const uint8_t *ptr = rp->cap; size_t offset = 0; uint8_t tag_len; uint8_t tag_type; if (status || !param) { error("Failed to read advertising features: %s (0x%02x)", mgmt_errstr(status), status); return; } if (sizeof(rp->cap_len) + rp->cap_len != length) { error("Controller capabilities malformed, size %zu != %u", sizeof(rp->cap_len) + rp->cap_len, length); return; } while (offset < rp->cap_len) { tag_len = ptr[offset++]; tag_type = ptr[offset++]; switch (tag_type) { case MGMT_CAP_LE_TX_PWR: if ((tag_len - sizeof(tag_type)) != 2*sizeof(manager->min_tx_power)) { error("TX power had unexpected length %d", tag_len); break; } memcpy(&manager->min_tx_power, &ptr[offset], tag_len); memcpy(&manager->max_tx_power, &ptr[offset+1], tag_len); } /* Step to the next entry */ offset += (tag_len - sizeof(tag_type)); } } static struct btd_adv_manager *manager_create(struct btd_adapter *adapter, struct mgmt *mgmt) { struct btd_adv_manager *manager; manager = new0(struct btd_adv_manager, 1); manager->adapter = adapter; manager->mgmt = mgmt_ref(mgmt); if (!manager->mgmt) { error("Failed to access management interface"); free(manager); return NULL; } manager->mgmt_index = btd_adapter_get_index(adapter); manager->clients = queue_new(); manager->supported_flags = MGMT_ADV_FLAG_LOCAL_NAME; manager->extended_add_cmds = btd_has_kernel_features(KERNEL_HAS_EXT_ADV_ADD_CMDS); manager->min_tx_power = ADV_TX_POWER_NO_PREFERENCE; manager->max_tx_power = ADV_TX_POWER_NO_PREFERENCE; if (!mgmt_send(manager->mgmt, MGMT_OP_READ_ADV_FEATURES, manager->mgmt_index, 0, NULL, read_adv_features_callback, manager, NULL)) { error("Failed to read advertising features"); goto fail; } /* Query controller capabilities. This will be used to display valid * advertising tx power range to the client. */ if (g_dbus_get_flags() & G_DBUS_FLAG_ENABLE_EXPERIMENTAL && btd_has_kernel_features(KERNEL_HAS_CONTROLLER_CAP_CMD)) mgmt_send(manager->mgmt, MGMT_OP_READ_CONTROLLER_CAP, manager->mgmt_index, 0, NULL, read_controller_cap_complete, manager, NULL); return manager; fail: manager_destroy(manager); return NULL; } struct btd_adv_manager *btd_adv_manager_new(struct btd_adapter *adapter, struct mgmt *mgmt) { struct btd_adv_manager *manager; if (!adapter || !mgmt) return NULL; manager = manager_create(adapter, mgmt); if (!manager) return NULL; DBG("LE Advertising Manager created for adapter: %s", adapter_get_path(adapter)); return manager; } void btd_adv_manager_destroy(struct btd_adv_manager *manager) { if (!manager) return; g_dbus_unregister_interface(btd_get_dbus_connection(), adapter_get_path(manager->adapter), LE_ADVERTISING_MGR_IFACE); manager_destroy(manager); } static void manager_refresh(void *data, void *user_data) { struct btd_adv_client *client = data; refresh_advertisement(client, user_data, NULL); } void btd_adv_manager_refresh(struct btd_adv_manager *manager) { if (!manager) return; queue_foreach(manager->clients, manager_refresh, NULL); }