1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtCore/QGlobalStatic>
5#include <QtCore/QLoggingCategory>
6#include <QtCore/QMap>
7#include <QtCore/QVersionNumber>
8#include <QtNetwork/private/qnet_unix_p.h>
9#include "bluez5_helper_p.h"
10#include "bluez_data_p.h"
11#include "objectmanager_p.h"
12#include "properties_p.h"
13#include "adapter1_bluez5_p.h"
14
15QT_BEGIN_NAMESPACE
16
17QT_IMPL_METATYPE_EXTERN(InterfaceList)
18QT_IMPL_METATYPE_EXTERN(ManufacturerDataList)
19QT_IMPL_METATYPE_EXTERN(ServiceDataList)
20QT_IMPL_METATYPE_EXTERN(ManagedObjectList)
21
22
23Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
24
25typedef enum Bluez5TestResultType
26{
27 BluezVersionUnknown,
28 BluezVersion5,
29 BluezNotAvailable
30} Bluez5TestResult;
31
32Q_GLOBAL_STATIC_WITH_ARGS(Bluez5TestResult, bluezVersion, (BluezVersionUnknown))
33Q_GLOBAL_STATIC_WITH_ARGS(QVersionNumber, bluezDaemonVersion, (QVersionNumber()))
34
35/*
36 Ensures that the DBus types are registered
37 */
38
39void initializeBluez5()
40{
41 if (*bluezVersion() == BluezVersionUnknown) {
42 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
43 QStringLiteral("/"),
44 QDBusConnection::systemBus());
45 qDBusRegisterMetaType<InterfaceList>();
46 qDBusRegisterMetaType<ManagedObjectList>();
47 qDBusRegisterMetaType<ManufacturerDataList>();
48 qDBusRegisterMetaType<ServiceDataList>();
49
50 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
51 reply.waitForFinished();
52 if (reply.isError()) {
53 *bluezVersion() = BluezNotAvailable;
54 qWarning() << "Cannot find a compatible running Bluez. "
55 "Please check the Bluez installation. "
56 "QtBluetooth requires at least BlueZ version 5.";
57 } else {
58 *bluezVersion() = BluezVersion5;
59 qCDebug(QT_BT_BLUEZ) << "Bluez 5 detected.";
60 }
61 }
62}
63
64/*
65 Checks that the mandatory Bluetooth HCI ioctls are offered
66 by Linux kernel. Returns \c true if the ictls are available; otherwise \c false.
67
68 Mandatory ioctls:
69 - HCIGETCONNLIST
70 - HCIGETDEVINFO
71 - HCIGETDEVLIST
72 */
73bool mandatoryHciIoctlsAvailable()
74{
75 // open hci socket
76 int hciSocket = ::qt_safe_socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
77 if (hciSocket < 0) {
78 qCWarning(QT_BT_BLUEZ) << "Cannot open HCI socket:" << qt_error_string(errno);
79 return false;
80 }
81
82 // check HCIGETDEVLIST & HCIGETDEVLIST
83 struct hci_dev_req *devRequest = nullptr;
84 struct hci_dev_list_req *devRequestList = nullptr;
85 struct hci_dev_info devInfo;
86 const int devListSize = sizeof(struct hci_dev_list_req)
87 + HCI_MAX_DEV * sizeof(struct hci_dev_req);
88
89 devRequestList = (hci_dev_list_req *) malloc(size: devListSize);
90 if (!devRequestList) {
91 qt_safe_close(fd: hciSocket);
92 return false; // if we cannot malloc nothing will help anyway
93 }
94
95 QScopedPointer<hci_dev_list_req, QScopedPointerPodDeleter> pDevList(devRequestList);
96 memset(s: pDevList.data(), c: 0, n: devListSize);
97 pDevList->dev_num = HCI_MAX_DEV;
98 devRequest = pDevList->dev_req;
99
100 if (qt_safe_ioctl(sockfd: hciSocket, HCIGETDEVLIST, arg: devRequestList) < 0) {
101 qt_safe_close(fd: hciSocket);
102 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVLIST:" << qt_error_string(errno);
103 return false;
104 }
105
106 if (devRequestList->dev_num > 0) {
107 devInfo.dev_id = devRequest->dev_id;
108 if (qt_safe_ioctl(sockfd: hciSocket, HCIGETDEVINFO, arg: &devInfo) < 0) {
109 qt_safe_close(fd: hciSocket);
110 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVINFO:" << qt_error_string(errno);
111 return false;
112 }
113 }
114
115 // check HCIGETCONNLIST
116 const int maxNoOfConnections = 20;
117 hci_conn_list_req *infoList = nullptr;
118 infoList = (hci_conn_list_req *)
119 malloc(size: sizeof(hci_conn_list_req) + maxNoOfConnections * sizeof(hci_conn_info));
120
121 if (!infoList) {
122 qt_safe_close(fd: hciSocket);
123 return false;
124 }
125
126 QScopedPointer<hci_conn_list_req, QScopedPointerPodDeleter> pInfoList(infoList);
127 pInfoList->conn_num = maxNoOfConnections;
128 pInfoList->dev_id = devInfo.dev_id;
129
130 if (qt_safe_ioctl(sockfd: hciSocket, HCIGETCONNLIST, arg: (void *) infoList) < 0) {
131 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETCONNLIST:" << qt_error_string(errno);
132 qt_safe_close(fd: hciSocket);
133 return false;
134 }
135
136 qt_safe_close(fd: hciSocket);
137 return true;
138}
139
140/*
141 * This function returns the version of bluetoothd in use on the system.
142 * This is required to determine which QLEControllerPrivate implementation
143 * is required. The following version tags are of significance:
144 *
145 * Version < 4.0 -> QLEControllerPrivateCommon
146 * Version < 5.42 -> QLEControllerPrivateBluez
147 * Version >= 5.42 -> QLEControllerPrivateBluezDBus
148 *
149 * This function utilizes a singleton pattern. It always returns a cached
150 * version tag which is determined on first call. This is necessary to
151 * avoid continuesly running the somewhat expensive tests.
152 *
153 * The function must never return a null QVersionNumber.
154 */
155QVersionNumber bluetoothdVersion()
156{
157 if (bluezDaemonVersion()->isNull()) {
158 // Register DBus specific meta types (copied from isBluez5())
159 // Not all code paths run through isBluez5() but still need the
160 // registration.
161 qDBusRegisterMetaType<InterfaceList>();
162 qDBusRegisterMetaType<ManagedObjectList>();
163 qDBusRegisterMetaType<ManufacturerDataList>();
164 qDBusRegisterMetaType<ServiceDataList>();
165
166 qCDebug(QT_BT_BLUEZ) << "Detecting bluetoothd version";
167 //Order of matching
168 // 1. Pick whatever the user decides via BLUETOOTH_FORCE_DBUS_LE_VERSION
169 // Set version to below version 5.42 to use custom/old GATT stack implementation
170 const QString version = qEnvironmentVariable(varName: "BLUETOOTH_FORCE_DBUS_LE_VERSION");
171 if (!version.isNull()) {
172 const QVersionNumber vn = QVersionNumber::fromString(string: version);
173 if (!vn.isNull()) {
174 *bluezDaemonVersion() = vn;
175 qCDebug(QT_BT_BLUEZ) << "Forcing Bluez LE API selection:"
176 << bluezDaemonVersion()->toString();
177 }
178 }
179
180 // 2. Find bluetoothd binary and check "bluetoothd --version"
181 if (bluezDaemonVersion()->isNull() && qt_haveLinuxProcfs()) {
182 QDBusConnection session = QDBusConnection::systemBus();
183 qint64 pid = session.interface()->servicePid(QStringLiteral("org.bluez")).value();
184 QByteArray buffer;
185
186 auto determineBinaryVersion = [](const QString &binary) -> QVersionNumber {
187 QProcess process;
188 process.start(program: binary, arguments: {QStringLiteral("--version")});
189 process.waitForFinished();
190
191 const QString version = QString::fromLocal8Bit(ba: process.readAll());
192 const QVersionNumber vn = QVersionNumber::fromString(string: version);
193 if (!vn.isNull())
194 qCDebug(QT_BT_BLUEZ) << "Detected bluetoothd version" << vn;
195 return vn;
196 };
197
198 //try reading /proc/<pid>/exe first -> requires process owner
199 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/exe";
200 const QString procExe = QStringLiteral("/proc/%1/exe").arg(a: pid);
201 const QVersionNumber vn = determineBinaryVersion(procExe);
202 if (!vn.isNull())
203 *bluezDaemonVersion() = vn;
204
205 if (bluezDaemonVersion()->isNull()) {
206 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/cmdline";
207 //try to reading /proc/<pid>/cmdline (does not require additional process rights)
208 QFile procFile(QStringLiteral("/proc/%1/cmdline").arg(a: pid));
209 if (procFile.open(flags: QIODevice::ReadOnly|QIODevice::Text)) {
210 buffer = procFile.readAll();
211 procFile.close();
212
213 // cmdline params separated by character \0 -> first is bluetoothd binary
214 const QString binary = QString::fromLocal8Bit(ba: buffer.split(sep: '\0').at(i: 0));
215 QFileInfo info(binary);
216 if (info.isExecutable())
217 *bluezDaemonVersion() = determineBinaryVersion(binary);
218 else
219 qCDebug(QT_BT_BLUEZ) << "Cannot determine bluetoothd version via cmdline:"
220 << binary;
221 }
222 }
223 }
224
225 // 3. Fall back to custom ATT backend, if possible?
226 if (bluezDaemonVersion()->isNull()) {
227 // Check mandatory HCI ioctls are available
228 if (mandatoryHciIoctlsAvailable()) {
229 // default 4.0 for now -> implies custom (G)ATT implementation
230 *bluezDaemonVersion() = QVersionNumber(4, 0);
231 }
232 }
233
234 // 4. Ultimate fallback -> enable dummy backend
235 if (bluezDaemonVersion()->isNull()) {
236 // version 3 represents disabled BTLE
237 // bluezDaemonVersion should not be null to avoid repeated version tests
238 *bluezDaemonVersion() = QVersionNumber(3, 0);
239 qCWarning(QT_BT_BLUEZ) << "Cannot determine bluetoothd version and required Bluetooth HCI ioctols";
240 qCWarning(QT_BT_BLUEZ) << "Disabling Qt Bluetooth LE feature";
241 }
242
243 qCDebug(QT_BT_BLUEZ) << "Bluetoothd:" << bluezDaemonVersion()->toString();
244 }
245
246 Q_ASSERT(!bluezDaemonVersion()->isNull());
247 return *bluezDaemonVersion();
248}
249
250struct AdapterData
251{
252public:
253 AdapterData() : reference(1), wasListeningAlready(false) {}
254
255 int reference;
256 bool wasListeningAlready;
257 OrgFreedesktopDBusPropertiesInterface *propteryListener = nullptr;
258};
259
260class QtBluezDiscoveryManagerPrivate
261{
262public:
263 QMap<QString, AdapterData *> references;
264 OrgFreedesktopDBusObjectManagerInterface *manager = nullptr;
265};
266
267Q_GLOBAL_STATIC(QtBluezDiscoveryManager, discoveryManager)
268
269/*!
270 \internal
271 \class QtBluezDiscoveryManager
272
273 This class manages the access to "org.bluez.Adapter1::Start/StopDiscovery.
274
275 The flag is a system flag. We want to ensure that the various Qt classes don't turn
276 the flag on and off and thereby get into their way. If some other system component
277 changes the flag (e.g. adapter removed) we notify all Qt classes about the change by emitting
278 \l discoveryInterrupted(QString). Classes should indicate this via an appropriate
279 error message to the user.
280
281 Once the signal was emitted, all existing requests for discovery mode on the same adapter
282 have to be renewed via \l registerDiscoveryInterest(QString).
283*/
284
285QtBluezDiscoveryManager::QtBluezDiscoveryManager(QObject *parent) :
286 QObject(parent)
287{
288 qCDebug(QT_BT_BLUEZ) << "Creating QtBluezDiscoveryManager";
289 d = new QtBluezDiscoveryManagerPrivate();
290
291 d->manager = new OrgFreedesktopDBusObjectManagerInterface(
292 QStringLiteral("org.bluez"), QStringLiteral("/"),
293 QDBusConnection::systemBus(), this);
294 connect(asender: d->manager, SIGNAL(InterfacesRemoved(QDBusObjectPath,QStringList)),
295 SLOT(InterfacesRemoved(QDBusObjectPath,QStringList)));
296}
297
298QtBluezDiscoveryManager::~QtBluezDiscoveryManager()
299{
300 qCDebug(QT_BT_BLUEZ) << "Destroying QtBluezDiscoveryManager";
301
302 const QList<QString> adapterPaths = d->references.keys();
303 for (const QString &adapterPath : adapterPaths) {
304 AdapterData *data = d->references.take(key: adapterPath);
305 delete data->propteryListener;
306
307 // turn discovery off if it wasn't on already
308 if (!data->wasListeningAlready) {
309 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
310 QDBusConnection::systemBus());
311 iface.StopDiscovery();
312 }
313
314 delete data;
315 }
316
317 delete d;
318}
319
320QtBluezDiscoveryManager *QtBluezDiscoveryManager::instance()
321{
322 return discoveryManager();
323}
324
325bool QtBluezDiscoveryManager::registerDiscoveryInterest(const QString &adapterPath)
326{
327 if (adapterPath.isEmpty())
328 return false;
329
330 // already monitored adapter? -> increase ref count -> done
331 if (d->references.contains(key: adapterPath)) {
332 d->references[adapterPath]->reference++;
333 return true;
334 }
335
336 AdapterData *data = new AdapterData();
337
338 OrgFreedesktopDBusPropertiesInterface *propIface = new OrgFreedesktopDBusPropertiesInterface(
339 QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus());
340 connect(sender: propIface, signal: &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
341 context: this, slot: &QtBluezDiscoveryManager::PropertiesChanged);
342 data->propteryListener = propIface;
343
344 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
345 QDBusConnection::systemBus());
346 data->wasListeningAlready = iface.discovering();
347
348 d->references[adapterPath] = data;
349
350 if (!data->wasListeningAlready)
351 iface.StartDiscovery();
352
353 return true;
354}
355
356void QtBluezDiscoveryManager::unregisterDiscoveryInterest(const QString &adapterPath)
357{
358 if (!d->references.contains(key: adapterPath))
359 return;
360
361 AdapterData *data = d->references[adapterPath];
362 data->reference--;
363
364 if (data->reference > 0) // more than one client requested discovery mode
365 return;
366
367 d->references.remove(key: adapterPath);
368 if (!data->wasListeningAlready) { // Qt turned discovery mode on, Qt has to turn it off again
369 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
370 QDBusConnection::systemBus());
371 iface.StopDiscovery();
372 }
373
374 delete data->propteryListener;
375 delete data;
376}
377
378//void QtBluezDiscoveryManager::dumpState() const
379//{
380// qCDebug(QT_BT_BLUEZ) << "-------------------------";
381// if (d->references.isEmpty()) {
382// qCDebug(QT_BT_BLUEZ) << "No running registration";
383// } else {
384// const QList<QString> paths = d->references.keys();
385// for (const QString &path : paths) {
386// qCDebug(QT_BT_BLUEZ) << path << "->" << d->references[path]->reference;
387// }
388// }
389// qCDebug(QT_BT_BLUEZ) << "-------------------------";
390//}
391
392void QtBluezDiscoveryManager::InterfacesRemoved(const QDBusObjectPath &object_path,
393 const QStringList &interfaces)
394{
395 if (!d->references.contains(key: object_path.path())
396 || !interfaces.contains(QStringLiteral("org.bluez.Adapter1")))
397 return;
398
399 removeAdapterFromMonitoring(dbusPath: object_path.path());
400}
401
402void QtBluezDiscoveryManager::PropertiesChanged(const QString &interface,
403 const QVariantMap &changed_properties,
404 const QStringList &invalidated_properties,
405 const QDBusMessage &)
406{
407 Q_UNUSED(invalidated_properties);
408
409 OrgFreedesktopDBusPropertiesInterface *propIface =
410 qobject_cast<OrgFreedesktopDBusPropertiesInterface *>(object: sender());
411
412 if (!propIface)
413 return;
414
415 if (interface == QStringLiteral("org.bluez.Adapter1")
416 && d->references.contains(key: propIface->path())
417 && changed_properties.contains(QStringLiteral("Discovering"))) {
418 bool isDiscovering = changed_properties.value(QStringLiteral("Discovering")).toBool();
419 if (!isDiscovering) {
420
421 /*
422 Once we stop the Discovering flag will switch a few ms later. This comes through this code
423 path. If a new device discovery is started while we are still
424 waiting for the flag change signal, then the new device discovery will be aborted prematurely.
425 To compensate we check whether there was renewed interest.
426 */
427
428 AdapterData *data = d->references[propIface->path()];
429 if (!data) {
430 removeAdapterFromMonitoring(dbusPath: propIface->path());
431 } else {
432 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), propIface->path(),
433 QDBusConnection::systemBus());
434 iface.StartDiscovery();
435 }
436 }
437 }
438}
439
440void QtBluezDiscoveryManager::removeAdapterFromMonitoring(const QString &dbusPath)
441{
442 // remove adapter from monitoring
443 AdapterData *data = d->references.take(key: dbusPath);
444 delete data->propteryListener;
445 delete data;
446
447 emit discoveryInterrupted(adapterPath: dbusPath);
448}
449
450/*
451 Finds the path for the local adapter with \a wantedAddress or an empty string
452 if no local adapter with the given address can be found.
453 If \a wantedAddress is \c null it returns the first/default adapter or an empty
454 string if none is available.
455
456 If \a ok is false the lookup was aborted due to a dbus error and this function
457 returns an empty string.
458 */
459QString findAdapterForAddress(const QBluetoothAddress &wantedAddress, bool *ok = nullptr)
460{
461 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
462 QStringLiteral("/"),
463 QDBusConnection::systemBus());
464
465 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
466 reply.waitForFinished();
467 if (reply.isError()) {
468 if (ok)
469 *ok = false;
470
471 return QString();
472 }
473
474 typedef QPair<QString, QBluetoothAddress> AddressForPathType;
475 QList<AddressForPathType> localAdapters;
476
477 ManagedObjectList managedObjectList = reply.value();
478 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
479 const QDBusObjectPath &path = it.key();
480 const InterfaceList &ifaceList = it.value();
481
482 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
483 const QString &iface = jt.key();
484
485 if (iface == QStringLiteral("org.bluez.Adapter1")) {
486 AddressForPathType pair;
487 pair.first = path.path();
488 pair.second = QBluetoothAddress(ifaceList.value(key: iface).value(
489 QStringLiteral("Address")).toString());
490 if (!pair.second.isNull())
491 localAdapters.append(t: pair);
492 break;
493 }
494 }
495 }
496
497 if (ok)
498 *ok = true;
499
500 if (localAdapters.isEmpty())
501 return QString(); // -> no local adapter found
502
503 if (wantedAddress.isNull())
504 return localAdapters.front().first; // -> return first found adapter
505
506 for (const AddressForPathType &pair : std::as_const(t&: localAdapters)) {
507 if (pair.second == wantedAddress)
508 return pair.first; // -> found local adapter with wanted address
509 }
510
511 return QString(); // nothing matching found
512}
513
514/*
515
516 Checks the presence of peripheral interface and returns path to the adapter
517
518*/
519
520QString adapterWithDBusPeripheralInterface(const QBluetoothAddress &localAddress)
521{
522 initializeBluez5();
523
524 // First find the object path to the desired adapter
525 bool ok = false;
526 const QString hostAdapterPath = findAdapterForAddress(wantedAddress: localAddress, ok: &ok);
527 if (!ok || hostAdapterPath.isEmpty())
528 return {};
529
530 // Then check if that adapter provides peripheral dbus interface
531 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
532 QStringLiteral("/"),
533 QDBusConnection::systemBus());
534 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
535 reply.waitForFinished();
536 if (reply.isError())
537 return {};
538
539 using namespace Qt::StringLiterals;
540 // For example /org/bluez/hci0 contains org.bluezLEAdvertisingManager1
541 const bool peripheralSupported = reply.value()
542 .value(key: QDBusObjectPath(hostAdapterPath))
543 .contains(key: "org.bluez.LEAdvertisingManager1"_L1);
544
545 qCDebug(QT_BT_BLUEZ) << "Peripheral role"
546 << (peripheralSupported ? "" : "not")
547 << "supported on" << hostAdapterPath;
548 return peripheralSupported ? hostAdapterPath : QString{};
549}
550
551/*
552 Removes every character that cannot be used in QDbusObjectPath
553
554 See QDbusUtil::isValidObjectPath(QString) for more details.
555 */
556QString sanitizeNameForDBus(const QString &text)
557{
558 QString appName = text;
559 for (qsizetype i = 0; i < appName.size(); ++i) {
560 ushort us = appName[i].unicode();
561 bool valid = (us >= 'a' && us <= 'z')
562 || (us >= 'A' && us <= 'Z')
563 || (us >= '0' && us <= '9')
564 || (us == '_');
565
566 if (!valid)
567 appName[i] = QLatin1Char('_');
568 }
569
570 return appName;
571}
572
573QT_END_NAMESPACE
574
575#include "moc_bluez5_helper_p.cpp"
576

source code of qtconnectivity/src/bluetooth/bluez/bluez5_helper.cpp