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/QLoggingCategory>
5
6#include <QtCore/qcoreapplication.h>
7
8#include "qbluetoothdevicediscoveryagent.h"
9#include "qbluetoothdevicediscoveryagent_p.h"
10#include "qbluetoothaddress.h"
11#include "qbluetoothuuid.h"
12
13#include "bluez/bluez5_helper_p.h"
14#include "bluez/objectmanager_p.h"
15#include "bluez/adapter1_bluez5_p.h"
16#include "bluez/device1_bluez5_p.h"
17#include "bluez/properties_p.h"
18#include "bluez/bluetoothmanagement_p.h"
19
20QT_BEGIN_NAMESPACE
21
22Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
23
24QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(
25 const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
26 adapterAddress(deviceAdapter),
27 q_ptr(parent)
28{
29 initializeBluez5();
30 manager = new OrgFreedesktopDBusObjectManagerInterface(
31 QStringLiteral("org.bluez"),
32 QStringLiteral("/"),
33 QDBusConnection::systemBus(), parent);
34 QObject::connect(sender: manager,
35 signal: &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded,
36 context: q_ptr,
37 slot: [this](const QDBusObjectPath &objectPath, InterfaceList interfacesAndProperties) {
38 this->_q_InterfacesAdded(object_path: objectPath, interfaces_and_properties: interfacesAndProperties);
39 });
40
41 // start private address monitoring
42 BluetoothManagement::instance();
43}
44
45QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
46{
47 delete adapter;
48}
49
50//TODO: Qt6 remove the pendingCancel/pendingStart logic as it is cumbersome.
51// It is a behavior change across all platforms and was initially done
52// for Bluez. The behavior should be similar to QBluetoothServiceDiscoveryAgent
53// PendingCancel creates issues whereby the agent is still shutting down
54// but isActive() below already returns false. This means the isActive() is
55// out of sync with the finished() and cancel() signal.
56
57bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
58{
59 if (pendingStart)
60 return true;
61 if (pendingCancel)
62 return false; //TODO Qt6: remove pending[Cancel|Start] logic (see comment above)
63
64 return adapter;
65}
66
67QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
68{
69 return (ClassicMethod | LowEnergyMethod);
70}
71
72void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
73{
74 if (pendingCancel == true) {
75 pendingStart = true;
76 return;
77 }
78
79 lastError = QBluetoothDeviceDiscoveryAgent::NoError;
80 errorString.clear();
81 discoveredDevices.clear();
82 devicesProperties.clear();
83
84 Q_Q(QBluetoothDeviceDiscoveryAgent);
85
86 bool ok = false;
87 const QString adapterPath = findAdapterForAddress(wantedAddress: adapterAddress, ok: &ok);
88 if (!ok || adapterPath.isEmpty()) {
89 qCWarning(QT_BT_BLUEZ) << "Cannot find Bluez 5 adapter for device search" << ok;
90 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
91 errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "Cannot find valid Bluetooth adapter.");
92 q->errorOccurred(error: lastError);
93 return;
94 }
95
96 adapter = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"), adapterPath,
97 QDBusConnection::systemBus());
98
99 if (!adapter->powered()) {
100 qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter";
101 lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
102 errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "Device is powered off");
103 delete adapter;
104 adapter = nullptr;
105 emit q->errorOccurred(error: lastError);
106 return;
107 }
108
109 QVariantMap map;
110 if (methods == (QBluetoothDeviceDiscoveryAgent::LowEnergyMethod|QBluetoothDeviceDiscoveryAgent::ClassicMethod))
111 map.insert(QStringLiteral("Transport"), QStringLiteral("auto"));
112 else if (methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)
113 map.insert(QStringLiteral("Transport"), QStringLiteral("le"));
114 else
115 map.insert(QStringLiteral("Transport"), QStringLiteral("bredr"));
116
117 // older BlueZ 5.x versions don't have this function
118 // filterReply returns UnknownMethod which we ignore
119 QDBusPendingReply<> filterReply = adapter->SetDiscoveryFilter(map);
120 filterReply.waitForFinished();
121 if (filterReply.isError()) {
122 if (filterReply.error().type() == QDBusError::Other
123 && filterReply.error().name() == QStringLiteral("org.bluez.Error.Failed")) {
124 qCDebug(QT_BT_BLUEZ) << "Discovery method" << methods << "not supported";
125 lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod;
126 errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "One or more device discovery methods "
127 "are not supported on this platform");
128 delete adapter;
129 adapter = nullptr;
130 emit q->errorOccurred(error: lastError);
131 return;
132 } else if (filterReply.error().type() != QDBusError::UnknownMethod) {
133 qCDebug(QT_BT_BLUEZ) << "SetDiscoveryFilter failed:" << filterReply.error();
134 }
135 }
136
137 QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapterPath: adapter->path());
138 QObject::connect(sender: QtBluezDiscoveryManager::instance(), signal: &QtBluezDiscoveryManager::discoveryInterrupted,
139 context: q, slot: [this](const QString &path){
140 this->_q_discoveryInterrupted(path);
141 });
142 OrgFreedesktopDBusPropertiesInterface *prop = new OrgFreedesktopDBusPropertiesInterface(
143 QStringLiteral("org.bluez"), QStringLiteral(""), QDBusConnection::systemBus());
144 QObject::connect(sender: prop, signal: &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
145 context: q, slot: [this](const QString &interface, const QVariantMap &changedProperties,
146 const QStringList &invalidatedProperties,
147 const QDBusMessage &signal) {
148 this->_q_PropertiesChanged(interface, path: signal.path(), changed_properties: changedProperties, invalidated_properties: invalidatedProperties);
149 });
150
151 // remember what we have to cleanup
152 propertyMonitors.append(t: prop);
153
154 // collect initial set of information
155 QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects();
156 reply.waitForFinished();
157 if (!reply.isError()) {
158 ManagedObjectList managedObjectList = reply.value();
159 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
160 const QDBusObjectPath &path = it.key();
161 const InterfaceList &ifaceList = it.value();
162
163 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
164 const QString &iface = jt.key();
165
166 if (iface == QStringLiteral("org.bluez.Device1")) {
167
168 if (path.path().indexOf(s: adapter->path()) != 0)
169 continue; //devices whose path doesn't start with same path we skip
170
171 deviceFound(devicePath: path.path(), properties: jt.value());
172 if (!isActive()) // Can happen if stop() was called from a slot in user code.
173 return;
174 }
175 }
176 }
177 }
178
179 // wait interval and sum up what was found
180 if (!discoveryTimer) {
181 discoveryTimer = new QTimer(q);
182 discoveryTimer->setSingleShot(true);
183 QObject::connect(sender: discoveryTimer, signal: &QTimer::timeout,
184 context: q, slot: [this]() {
185 this->_q_discoveryFinished();
186 });
187 }
188
189 if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
190 discoveryTimer->setInterval(lowEnergySearchTimeout);
191 discoveryTimer->start();
192 }
193}
194
195void QBluetoothDeviceDiscoveryAgentPrivate::stop()
196{
197 if (!adapter)
198 return;
199
200 qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO;
201 pendingCancel = true;
202 pendingStart = false;
203 _q_discoveryFinished();
204}
205
206// Returns invalid QBluetoothDeviceInfo in case of error
207static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap& properties)
208{
209 const QBluetoothAddress btAddress(properties[QStringLiteral("Address")].toString());
210 if (btAddress.isNull())
211 return QBluetoothDeviceInfo();
212
213 const QString btName = properties[QStringLiteral("Alias")].toString();
214 quint32 btClass = properties[QStringLiteral("Class")].toUInt();
215
216 QBluetoothDeviceInfo deviceInfo(btAddress, btName, btClass);
217 deviceInfo.setRssi(qvariant_cast<short>(v: properties[QStringLiteral("RSSI")]));
218
219 QList<QBluetoothUuid> uuids;
220 bool foundLikelyLowEnergyUuid = false;
221 const QStringList foundUuids = qvariant_cast<QStringList>(v: properties[QStringLiteral("UUIDs")]);
222 for (const auto &u: foundUuids) {
223 const QBluetoothUuid id(u);
224 if (id.isNull())
225 continue;
226
227 if (!foundLikelyLowEnergyUuid) {
228 //once we found one BTLE service we are done
229 bool ok = false;
230 quint16 shortId = id.toUInt16(ok: &ok);
231 quint16 genericAccessInt = static_cast<quint16>(QBluetoothUuid::ServiceClassUuid::GenericAccess);
232 if (ok && ((shortId & genericAccessInt) == genericAccessInt))
233 foundLikelyLowEnergyUuid = true;
234 }
235 uuids.append(t: id);
236 }
237 deviceInfo.setServiceUuids(uuids);
238
239 if (!btClass) {
240 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
241 } else {
242 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
243 if (foundLikelyLowEnergyUuid)
244 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration);
245 }
246
247 const ManufacturerDataList deviceManufacturerData = qdbus_cast<ManufacturerDataList>(v: properties[QStringLiteral("ManufacturerData")]);
248 const QList<quint16> keysManufacturer = deviceManufacturerData.keys();
249 for (quint16 key : keysManufacturer)
250 deviceInfo.setManufacturerData(
251 manufacturerId: key, data: deviceManufacturerData.value(key).variant().toByteArray());
252
253 const ServiceDataList deviceServiceData =
254 qdbus_cast<ServiceDataList>(v: properties[QStringLiteral("ServiceData")]);
255 const QList<QString> keysService = deviceServiceData.keys();
256 for (QString key : keysService)
257 deviceInfo.setServiceData(serviceId: QBluetoothUuid(key),
258 data: deviceServiceData.value(key).variant().toByteArray());
259
260 return deviceInfo;
261}
262
263void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QString &devicePath,
264 const QVariantMap &properties)
265{
266 Q_Q(QBluetoothDeviceDiscoveryAgent);
267
268 if (!q->isActive())
269 return;
270
271 auto deviceAdapter = qvariant_cast<QDBusObjectPath>(v: properties[QStringLiteral("Adapter")]);
272 if (deviceAdapter.path() != adapter->path())
273 return;
274
275 // read information
276 QBluetoothDeviceInfo deviceInfo = createDeviceInfoFromBluez5Device(properties);
277 if (!deviceInfo.isValid()) // no point reporting an empty address
278 return;
279
280 qCDebug(QT_BT_BLUEZ) << "Discovered: " << deviceInfo.name() << deviceInfo.address()
281 << "Num UUIDs" << deviceInfo.serviceUuids().size()
282 << "total device" << discoveredDevices.size() << "cached"
283 << "RSSI" << deviceInfo.rssi()
284 << "Num ManufacturerData" << deviceInfo.manufacturerData().size()
285 << "Num ServiceData" << deviceInfo.serviceData().size();
286
287 // Cache the properties so we do not have to access dbus every time to get a value
288 devicesProperties[devicePath] = properties;
289
290 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
291 if (discoveredDevices[i].address() == deviceInfo.address()) {
292 if (lowEnergySearchTimeout > 0 && discoveredDevices[i] == deviceInfo) {
293 qCDebug(QT_BT_BLUEZ) << "Duplicate: " << deviceInfo.address();
294 return;
295 }
296 discoveredDevices.replace(i, t: deviceInfo);
297
298 emit q->deviceDiscovered(info: deviceInfo);
299 return; // this works if the list doesn't contain duplicates. Don't let it.
300 }
301 }
302
303 discoveredDevices.append(t: deviceInfo);
304 emit q->deviceDiscovered(info: deviceInfo);
305}
306
307void QBluetoothDeviceDiscoveryAgentPrivate::_q_InterfacesAdded(const QDBusObjectPath &object_path,
308 InterfaceList interfaces_and_properties)
309{
310 Q_Q(QBluetoothDeviceDiscoveryAgent);
311
312 if (!q->isActive())
313 return;
314
315 if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) {
316 // device interfaces belonging to different adapter
317 // will be filtered out by deviceFound();
318 deviceFound(devicePath: object_path.path(),
319 properties: interfaces_and_properties[QStringLiteral("org.bluez.Device1")]);
320 }
321}
322
323void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished()
324{
325 Q_Q(QBluetoothDeviceDiscoveryAgent);
326
327 if (discoveryTimer)
328 discoveryTimer->stop();
329
330 QtBluezDiscoveryManager::instance()->disconnect(receiver: q);
331 QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapterPath: adapter->path());
332
333 qDeleteAll(c: propertyMonitors);
334 propertyMonitors.clear();
335
336 delete adapter;
337 adapter = nullptr;
338
339 if (pendingCancel && !pendingStart) {
340 pendingCancel = false;
341 emit q->canceled();
342 } else if (pendingStart) {
343 pendingStart = false;
344 pendingCancel = false;
345 start(methods: QBluetoothDeviceDiscoveryAgent::ClassicMethod
346 | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
347 } else {
348 emit q->finished();
349 }
350}
351
352void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryInterrupted(const QString &path)
353{
354 Q_Q(QBluetoothDeviceDiscoveryAgent);
355
356 if (!q->isActive())
357 return;
358
359 if (path == adapter->path()) {
360 qCWarning(QT_BT_BLUEZ) << "Device discovery aborted due to unexpected adapter changes from another process.";
361
362 if (discoveryTimer)
363 discoveryTimer->stop();
364
365 QtBluezDiscoveryManager::instance()->disconnect(receiver: q);
366 // no need to call unregisterDiscoveryInterest since QtBluezDiscoveryManager
367 // does this automatically when emitting discoveryInterrupted(QString) signal
368
369 delete adapter;
370 adapter = nullptr;
371
372 errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "Bluetooth adapter error");
373 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
374 emit q->errorOccurred(error: lastError);
375 }
376}
377
378void QBluetoothDeviceDiscoveryAgentPrivate::_q_PropertiesChanged(const QString &interface,
379 const QString &path,
380 const QVariantMap &changed_properties,
381 const QStringList &invalidated_properties)
382{
383 Q_Q(QBluetoothDeviceDiscoveryAgent);
384 if (interface != QStringLiteral("org.bluez.Device1"))
385 return;
386
387 if (!devicesProperties.contains(key: path))
388 return;
389
390 // Update the cached properties before checking changed_properties for RSSI and ManufacturerData
391 // so the cached properties are always up to date.
392 QVariantMap & properties = devicesProperties[path];
393 for (QVariantMap::const_iterator it = changed_properties.constBegin();
394 it != changed_properties.constEnd(); ++it) {
395 properties[it.key()] = it.value();
396 }
397
398 for (const QString & property : invalidated_properties)
399 properties.remove(key: property);
400
401 const auto info = createDeviceInfoFromBluez5Device(properties);
402 if (!info.isValid())
403 return;
404
405 if (changed_properties.contains(QStringLiteral("RSSI"))
406 || changed_properties.contains(QStringLiteral("ManufacturerData"))) {
407
408 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
409 if (discoveredDevices[i].address() == info.address()) {
410 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
411 if (changed_properties.contains(QStringLiteral("RSSI"))) {
412 qCDebug(QT_BT_BLUEZ) << "Updating RSSI for" << info.address()
413 << changed_properties.value(QStringLiteral("RSSI"));
414 discoveredDevices[i].setRssi(
415 changed_properties.value(QStringLiteral("RSSI")).toInt());
416 updatedFields.setFlag(flag: QBluetoothDeviceInfo::Field::RSSI);
417 }
418 if (changed_properties.contains(QStringLiteral("ManufacturerData"))) {
419 qCDebug(QT_BT_BLUEZ) << "Updating ManufacturerData for" << info.address();
420 ManufacturerDataList changedManufacturerData =
421 qdbus_cast< ManufacturerDataList >(v: changed_properties.value(QStringLiteral("ManufacturerData")));
422
423 const QList<quint16> keys = changedManufacturerData.keys();
424 bool wasNewValue = false;
425 for (quint16 key : keys) {
426 bool added = discoveredDevices[i].setManufacturerData(manufacturerId: key, data: changedManufacturerData.value(key).variant().toByteArray());
427 wasNewValue = (wasNewValue || added);
428 }
429
430 if (wasNewValue)
431 updatedFields.setFlag(flag: QBluetoothDeviceInfo::Field::ManufacturerData);
432 }
433
434 if (lowEnergySearchTimeout > 0) {
435 if (discoveredDevices[i] != info) { // field other than manufacturer or rssi changed
436 if (discoveredDevices.at(i).name() == info.name()) {
437 qCDebug(QT_BT_BLUEZ) << "Almost Duplicate " << info.address()
438 << info.name() << "- replacing in place";
439 discoveredDevices.replace(i, t: info);
440 emit q->deviceDiscovered(info);
441 }
442 } else {
443 if (!updatedFields.testFlag(flag: QBluetoothDeviceInfo::Field::None))
444 emit q->deviceUpdated(info: discoveredDevices[i], updatedFields);
445 }
446
447 return;
448 }
449
450 discoveredDevices.replace(i, t: info);
451 emit q_ptr->deviceDiscovered(info: discoveredDevices[i]);
452
453 if (!updatedFields.testFlag(flag: QBluetoothDeviceInfo::Field::None))
454 emit q->deviceUpdated(info: discoveredDevices[i], updatedFields);
455 return;
456 }
457 }
458 }
459}
460QT_END_NAMESPACE
461

source code of qtconnectivity/src/bluetooth/qbluetoothdevicediscoveryagent_bluez.cpp