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 "qbluetoothservicediscoveryagent.h"
5#include "qbluetoothservicediscoveryagent_p.h"
6
7#include "bluez/bluez5_helper_p.h"
8#include "bluez/objectmanager_p.h"
9#include "bluez/adapter1_bluez5_p.h"
10
11#include <QtCore/QFile>
12#include <QtCore/QLibraryInfo>
13#include <QtCore/QLoggingCategory>
14#include <QtCore/QProcess>
15#include <QtCore/QScopeGuard>
16
17#include <QtDBus/QDBusPendingCallWatcher>
18
19QT_BEGIN_NAMESPACE
20
21Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
22
23QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate(
24 QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter)
25: error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive),
26 mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false),
27 q_ptr(qp)
28{
29 initializeBluez5();
30 manager = new OrgFreedesktopDBusObjectManagerInterface(
31 QStringLiteral("org.bluez"), QStringLiteral("/"), QDBusConnection::systemBus());
32 qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>();
33}
34
35QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate()
36{
37 delete manager;
38}
39
40void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address)
41{
42 Q_Q(QBluetoothServiceDiscoveryAgent);
43
44 qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode();
45
46 if (foundHostAdapterPath.isEmpty()) {
47 // check that we match adapter addresses or use first if it wasn't specified
48
49 bool ok = false;
50 foundHostAdapterPath = findAdapterForAddress(wantedAddress: m_deviceAdapterAddress, ok: &ok);
51 if (!ok) {
52 discoveredDevices.clear();
53 error = QBluetoothServiceDiscoveryAgent::InputOutputError;
54 errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "Cannot access adapter during service discovery");
55 emit q->errorOccurred(error);
56 _q_serviceDiscoveryFinished();
57 return;
58 }
59
60 if (foundHostAdapterPath.isEmpty()) {
61 // Cannot find a local adapter
62 // Abort any outstanding discoveries
63 discoveredDevices.clear();
64
65 error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError;
66 errorString = QBluetoothServiceDiscoveryAgent::tr(s: "Cannot find local Bluetooth adapter");
67 emit q->errorOccurred(error);
68 _q_serviceDiscoveryFinished();
69
70 return;
71 }
72 }
73
74 // ensure we didn't go offline yet
75 OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez"),
76 foundHostAdapterPath, QDBusConnection::systemBus());
77 if (!adapter.powered()) {
78 discoveredDevices.clear();
79
80 error = QBluetoothServiceDiscoveryAgent::PoweredOffError;
81 errorString = QBluetoothServiceDiscoveryAgent::tr(s: "Local device is powered off");
82 emit q->errorOccurred(error);
83
84 _q_serviceDiscoveryFinished();
85 return;
86 }
87
88 if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) {
89 performMinimalServiceDiscovery(deviceAddress: address);
90 } else {
91 runExternalSdpScan(remoteAddress: address, localAddress: QBluetoothAddress(adapter.address()));
92 }
93}
94
95/* Bluez 5
96 * src/tools/sdpscanner performs an SDP scan. This is
97 * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2.
98 */
99void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan(
100 const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress)
101{
102 Q_Q(QBluetoothServiceDiscoveryAgent);
103
104 if (!sdpScannerProcess) {
105 const QString binPath = QLibraryInfo::path(p: QLibraryInfo::LibraryExecutablesPath);
106 QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner"));
107 if (!fileInfo.exists() || !fileInfo.isExecutable()) {
108 _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::InputOutputError,
109 errorDescription: QBluetoothServiceDiscoveryAgent::tr(s: "Unable to find sdpscanner"),
110 xmlRecords: QStringList());
111 qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:"
112 << fileInfo.canonicalFilePath();
113 return;
114 }
115
116 sdpScannerProcess = new QProcess(q);
117 sdpScannerProcess->setReadChannel(QProcess::StandardOutput);
118 if (QT_BT_BLUEZ().isDebugEnabled())
119 sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel);
120 sdpScannerProcess->setProgram(fileInfo.canonicalFilePath());
121 q->connect(sender: sdpScannerProcess,
122 signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished),
123 context: q, slot: [this](int exitCode, QProcess::ExitStatus status){
124 this->_q_sdpScannerDone(exitCode, status);
125 });
126 }
127
128 QStringList arguments;
129 arguments << remoteAddress.toString() << localAddress.toString();
130
131 // No filter implies PUBLIC_BROWSE_GROUP based SDP scan
132 if (!uuidFilter.isEmpty()) {
133 arguments << QLatin1String("-u"); // cmd line option for list of uuids
134 for (const QBluetoothUuid& uuid : std::as_const(t&: uuidFilter))
135 arguments << uuid.toString();
136 }
137
138 sdpScannerProcess->setArguments(arguments);
139 sdpScannerProcess->start();
140}
141
142void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status)
143{
144 if (status != QProcess::NormalExit || exitCode != 0) {
145 qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode;
146 if (singleDevice) {
147 _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::InputOutputError,
148 errorDescription: QBluetoothServiceDiscoveryAgent::tr(s: "Unable to perform SDP scan"),
149 xmlRecords: QStringList());
150 } else {
151 // go to next device
152 _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::NoError, errorDescription: QString(), xmlRecords: QStringList());
153 }
154 return;
155 }
156
157 QStringList xmlRecords;
158 const QByteArray utf8Data = QByteArray::fromBase64(base64: sdpScannerProcess->readAllStandardOutput());
159 const QByteArrayView utf8View = utf8Data;
160
161 // split the various xml docs up
162 constexpr auto matcher = qMakeStaticByteArrayMatcher(pattern: "<?xml");
163 qsizetype next;
164 qsizetype start = matcher.indexIn(haystack: utf8View, from: 0);
165 if (start != -1) {
166 do {
167 next = matcher.indexIn(haystack: utf8View, from: start + 1);
168 if (next != -1)
169 xmlRecords.append(t: QString::fromUtf8(utf8: utf8View.sliced(pos: start, n: next - start)));
170 else
171 xmlRecords.append(t: QString::fromUtf8(utf8: utf8View.sliced(pos: start)));
172 start = next;
173 } while ( start != -1);
174 }
175
176 _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::NoError, errorDescription: QString(), xmlRecords);
177}
178
179void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode,
180 const QString &errorDescription,
181 const QStringList &xmlRecords)
182{
183 Q_Q(QBluetoothServiceDiscoveryAgent);
184
185 if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) {
186 qCWarning(QT_BT_BLUEZ) << "SDP search failed for"
187 << (!discoveredDevices.isEmpty()
188 ? discoveredDevices.at(i: 0).address().toString()
189 : QStringLiteral("<Unknown>"));
190 // We have an error which we need to indicate and stop further processing
191 discoveredDevices.clear();
192 error = errorCode;
193 errorString = errorDescription;
194 emit q->errorOccurred(error);
195 } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) {
196 for (const QString &record : xmlRecords) {
197 QBluetoothServiceInfo serviceInfo = parseServiceXml(xml: record);
198
199 //apply uuidFilter
200 if (!uuidFilter.isEmpty()) {
201 bool serviceNameMatched = uuidFilter.contains(t: serviceInfo.serviceUuid());
202 bool serviceClassMatched = false;
203 const QList<QBluetoothUuid> serviceClassUuids
204 = serviceInfo.serviceClassUuids();
205 for (const QBluetoothUuid &id : serviceClassUuids) {
206 if (uuidFilter.contains(t: id)) {
207 serviceClassMatched = true;
208 break;
209 }
210 }
211
212 if (!serviceNameMatched && !serviceClassMatched)
213 continue;
214 }
215
216 if (!serviceInfo.isValid())
217 continue;
218
219 // Bluez sdpscanner declares custom uuids into the service class uuid list.
220 // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids()
221 // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid
222 const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids();
223 for (const QBluetoothUuid &id : serviceClassUuids) {
224 if (id.minimumSize() == 16) {
225 serviceInfo.setServiceUuid(id);
226 if (serviceInfo.serviceName().isEmpty()) {
227 serviceInfo.setServiceName(
228 QBluetoothServiceDiscoveryAgent::tr(s: "Custom Service"));
229 }
230 QBluetoothServiceInfo::Sequence modSeq =
231 serviceInfo.attribute(attributeId: QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>();
232 modSeq.removeOne(t: QVariant::fromValue(value: id));
233 serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ServiceClassIds, value: modSeq);
234 break;
235 }
236 }
237
238 if (!isDuplicatedService(serviceInfo)) {
239 discoveredServices.append(t: serviceInfo);
240 qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(i: 0).address().toString()
241 << serviceInfo.serviceName() << serviceInfo.serviceUuid()
242 << ">>>" << serviceInfo.serviceClassUuids();
243 // Use queued connection to allow us finish the service looping; the application
244 // might call stop() when it has detected the service-of-interest.
245 QMetaObject::invokeMethod(obj: q, member: "serviceDiscovered", c: Qt::QueuedConnection,
246 Q_ARG(QBluetoothServiceInfo, serviceInfo));
247 }
248 }
249 }
250
251 _q_serviceDiscoveryFinished();
252}
253
254void QBluetoothServiceDiscoveryAgentPrivate::stop()
255{
256 qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called";
257
258 discoveredDevices.clear();
259 setDiscoveryState(Inactive);
260
261 // must happen after discoveredDevices.clear() above to avoid retrigger of next scan
262 // while waitForFinished() is waiting
263 if (sdpScannerProcess) { // Bluez 5
264 if (sdpScannerProcess->state() != QProcess::NotRunning) {
265 sdpScannerProcess->kill();
266 sdpScannerProcess->waitForFinished();
267 }
268 }
269
270 Q_Q(QBluetoothServiceDiscoveryAgent);
271 emit q->canceled();
272}
273
274QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml(
275 const QString& xmlRecord)
276{
277 QXmlStreamReader xml(xmlRecord);
278
279 QBluetoothServiceInfo serviceInfo;
280 serviceInfo.setDevice(discoveredDevices.at(i: 0));
281
282 while (!xml.atEnd()) {
283 xml.readNext();
284
285 if (xml.tokenType() == QXmlStreamReader::StartElement &&
286 xml.name() == QLatin1String("attribute")) {
287 quint16 attributeId =
288 xml.attributes().value(qualifiedName: QLatin1String("id")).toUShort(ok: nullptr, base: 0);
289
290 if (xml.readNextStartElement()) {
291 const QVariant value = readAttributeValue(xml);
292 serviceInfo.setAttribute(attributeId, value);
293 }
294 }
295 }
296
297 return serviceInfo;
298}
299
300// Bluez 5
301void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress)
302{
303 if (foundHostAdapterPath.isEmpty()) {
304 _q_serviceDiscoveryFinished();
305 return;
306 }
307
308 Q_Q(QBluetoothServiceDiscoveryAgent);
309
310 QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects();
311 reply.waitForFinished();
312 if (reply.isError()) {
313 if (singleDevice) {
314 error = QBluetoothServiceDiscoveryAgent::InputOutputError;
315 errorString = reply.error().message();
316 emit q->errorOccurred(error);
317 }
318 _q_serviceDiscoveryFinished();
319 return;
320 }
321
322 QStringList uuidStrings;
323
324 ManagedObjectList managedObjectList = reply.value();
325 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
326 const InterfaceList &ifaceList = it.value();
327
328 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
329 const QString &iface = jt.key();
330 const QVariantMap &ifaceValues = jt.value();
331
332 if (iface == QStringLiteral("org.bluez.Device1")) {
333 if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address")).toString()) {
334 uuidStrings = ifaceValues.value(QStringLiteral("UUIDs")).toStringList();
335 break;
336 }
337 }
338 }
339 if (!uuidStrings.isEmpty())
340 break;
341 }
342
343 if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) {
344 qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString();
345 // nothing found -> go to next uuid
346 _q_serviceDiscoveryFinished();
347 return;
348 }
349
350 qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings;
351
352 QBluetoothUuid uuid;
353 for (qsizetype i = 0; i < uuidStrings.size(); ++i) {
354 uuid = QBluetoothUuid(uuidStrings.at(i));
355 if (uuid.isNull())
356 continue;
357
358 //apply uuidFilter
359 if (!uuidFilter.isEmpty() && !uuidFilter.contains(t: uuid))
360 continue;
361
362 QBluetoothServiceInfo serviceInfo;
363 serviceInfo.setDevice(discoveredDevices.at(i: 0));
364
365 if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID
366 serviceInfo.setServiceUuid(uuid);
367 serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr(s: "Custom Service"));
368 } else {
369 // set uuid as service class id
370 QBluetoothServiceInfo::Sequence classId;
371 classId << QVariant::fromValue(value: uuid);
372 serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ServiceClassIds, value: classId);
373 QBluetoothUuid::ServiceClassUuid clsId
374 = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff);
375 serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(uuid: clsId));
376 }
377
378 QBluetoothServiceInfo::Sequence protocolDescriptorList;
379 {
380 QBluetoothServiceInfo::Sequence protocol;
381 protocol << QVariant::fromValue(value: QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap));
382 protocolDescriptorList.append(t: QVariant::fromValue(value: protocol));
383 }
384 {
385 QBluetoothServiceInfo::Sequence protocol;
386 protocol << QVariant::fromValue(value: QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Att));
387 protocolDescriptorList.append(t: QVariant::fromValue(value: protocol));
388 }
389 serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ProtocolDescriptorList, value: protocolDescriptorList);
390
391 //don't include the service if we already discovered it before
392 if (!isDuplicatedService(serviceInfo)) {
393 discoveredServices << serviceInfo;
394 qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(i: 0).address().toString()
395 << serviceInfo.serviceName();
396 emit q->serviceDiscovered(info: serviceInfo);
397 }
398 }
399
400 _q_serviceDiscoveryFinished();
401}
402
403QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml)
404{
405 auto skippingCurrentElementByDefault = qScopeGuard(f: [&] { xml.skipCurrentElement(); });
406
407 if (xml.name() == QLatin1String("boolean")) {
408 return xml.attributes().value(qualifiedName: QLatin1String("value")) == QLatin1String("true");
409 } else if (xml.name() == QLatin1String("uint8")) {
410 quint8 value = xml.attributes().value(qualifiedName: QLatin1String("value")).toUShort(ok: nullptr, base: 0);
411 return value;
412 } else if (xml.name() == QLatin1String("uint16")) {
413 quint16 value = xml.attributes().value(qualifiedName: QLatin1String("value")).toUShort(ok: nullptr, base: 0);
414 return value;
415 } else if (xml.name() == QLatin1String("uint32")) {
416 quint32 value = xml.attributes().value(qualifiedName: QLatin1String("value")).toUInt(ok: nullptr, base: 0);
417 return value;
418 } else if (xml.name() == QLatin1String("uint64")) {
419 quint64 value = xml.attributes().value(qualifiedName: QLatin1String("value")).toULongLong(ok: nullptr, base: 0);
420 return value;
421 } else if (xml.name() == QLatin1String("uuid")) {
422 QBluetoothUuid uuid;
423 const QStringView value = xml.attributes().value(qualifiedName: QLatin1String("value"));
424 if (value.startsWith(s: QLatin1String("0x"))) {
425 if (value.size() == 6) {
426 quint16 v = value.toUShort(ok: nullptr, base: 0);
427 uuid = QBluetoothUuid(v);
428 } else if (value.size() == 10) {
429 quint32 v = value.toUInt(ok: nullptr, base: 0);
430 uuid = QBluetoothUuid(v);
431 }
432 } else {
433 uuid = QBluetoothUuid(value.toString());
434 }
435 return QVariant::fromValue(value: uuid);
436 } else if (xml.name() == QLatin1String("text") || xml.name() == QLatin1String("url")) {
437 const QStringView value = xml.attributes().value(qualifiedName: QLatin1String("value"));
438 if (xml.attributes().value(qualifiedName: QLatin1String("encoding")) == QLatin1String("hex"))
439 return QString::fromUtf8(ba: QByteArray::fromHex(hexEncoded: value.toLatin1()));
440 return value.toString();
441 } else if (xml.name() == QLatin1String("sequence")) {
442 QBluetoothServiceInfo::Sequence sequence;
443
444 skippingCurrentElementByDefault.dismiss(); // we skip several elements here
445
446 while (xml.readNextStartElement()) {
447 QVariant value = readAttributeValue(xml);
448 sequence.append(t: value);
449 }
450
451 return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(value: sequence);
452 } else {
453 qCWarning(QT_BT_BLUEZ) << "unknown attribute type"
454 << xml.name()
455 << xml.attributes().value(qualifiedName: QLatin1String("value"));
456 return QVariant();
457 }
458}
459
460QT_END_NAMESPACE
461

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