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