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 | |
19 | QT_BEGIN_NAMESPACE |
20 | |
21 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
22 | |
23 | QBluetoothServiceDiscoveryAgentPrivate::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 | |
35 | QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() |
36 | { |
37 | delete manager; |
38 | } |
39 | |
40 | void 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 | */ |
99 | void 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 | |
142 | void 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 | |
179 | void 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 | |
254 | void 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 | |
274 | QBluetoothServiceInfo 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 |
301 | void 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 | |
403 | QVariant 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 | |
460 | QT_END_NAMESPACE |
461 | |