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 "qbluetoothservicediscoveryagent.h" |
41 | #include "qbluetoothservicediscoveryagent_p.h" |
42 | |
43 | #include "bluez/manager_p.h" |
44 | #include "bluez/adapter_p.h" |
45 | #include "bluez/device_p.h" |
46 | #include "bluez/bluez5_helper_p.h" |
47 | #include "bluez/objectmanager_p.h" |
48 | #include "bluez/adapter1_bluez5_p.h" |
49 | |
50 | #include <QtCore/QFile> |
51 | #include <QtCore/QLibraryInfo> |
52 | #include <QtCore/QLoggingCategory> |
53 | #include <QtCore/QProcess> |
54 | #include <QtDBus/QDBusPendingCallWatcher> |
55 | #include <QtConcurrent/QtConcurrentRun> |
56 | |
57 | QT_BEGIN_NAMESPACE |
58 | |
59 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
60 | |
61 | QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( |
62 | QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter) |
63 | : error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive), |
64 | mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false), |
65 | q_ptr(qp) |
66 | { |
67 | if (isBluez5()) { |
68 | managerBluez5 = new OrgFreedesktopDBusObjectManagerInterface( |
69 | QStringLiteral("org.bluez" ), QStringLiteral("/" ), |
70 | QDBusConnection::systemBus()); |
71 | qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>(); |
72 | } else { |
73 | qRegisterMetaType<ServiceMap>(); |
74 | qDBusRegisterMetaType<ServiceMap>(); |
75 | |
76 | manager = new OrgBluezManagerInterface(QStringLiteral("org.bluez" ), QStringLiteral("/" ), |
77 | QDBusConnection::systemBus()); |
78 | } |
79 | } |
80 | |
81 | QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() |
82 | { |
83 | delete device; |
84 | delete manager; |
85 | delete managerBluez5; |
86 | delete adapter; |
87 | } |
88 | |
89 | void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) |
90 | { |
91 | Q_Q(QBluetoothServiceDiscoveryAgent); |
92 | |
93 | qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode(); |
94 | |
95 | if (managerBluez5) { |
96 | startBluez5(address); |
97 | return; |
98 | } |
99 | |
100 | QDBusPendingReply<QDBusObjectPath> reply; |
101 | if (m_deviceAdapterAddress.isNull()) |
102 | reply = manager->DefaultAdapter(); |
103 | else |
104 | reply = manager->FindAdapter(m_deviceAdapterAddress.toString()); |
105 | |
106 | reply.waitForFinished(); |
107 | if (reply.isError()) { |
108 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
109 | errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to find appointed local adapter" ); |
110 | emit q->error(error); |
111 | _q_serviceDiscoveryFinished(); |
112 | return; |
113 | } |
114 | |
115 | adapter = new OrgBluezAdapterInterface(QStringLiteral("org.bluez" ), reply.value().path(), |
116 | QDBusConnection::systemBus()); |
117 | |
118 | if (m_deviceAdapterAddress.isNull()) { |
119 | QDBusPendingReply<QVariantMap> reply = adapter->GetProperties(); |
120 | reply.waitForFinished(); |
121 | if (!reply.isError()) { |
122 | const QBluetoothAddress path_address(reply.value().value(QStringLiteral("Address" )).toString()); |
123 | m_deviceAdapterAddress = path_address; |
124 | } |
125 | } |
126 | |
127 | QDBusPendingReply<QDBusObjectPath> deviceObjectPath = adapter->FindDevice(address.toString()); |
128 | |
129 | QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(deviceObjectPath, q); |
130 | watcher->setProperty("_q_BTaddress" , QVariant::fromValue(address)); |
131 | QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
132 | q, [this](QDBusPendingCallWatcher *watcher){ |
133 | this->_q_foundDevice(watcher); |
134 | }); |
135 | } |
136 | |
137 | // Bluez 5 |
138 | void QBluetoothServiceDiscoveryAgentPrivate::startBluez5(const QBluetoothAddress &address) |
139 | { |
140 | Q_Q(QBluetoothServiceDiscoveryAgent); |
141 | |
142 | if (foundHostAdapterPath.isEmpty()) { |
143 | // check that we match adapter addresses or use first if it wasn't specified |
144 | |
145 | bool ok = false; |
146 | foundHostAdapterPath = findAdapterForAddress(m_deviceAdapterAddress, &ok); |
147 | if (!ok) { |
148 | discoveredDevices.clear(); |
149 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
150 | errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot access adapter during service discovery" ); |
151 | emit q->error(error); |
152 | _q_serviceDiscoveryFinished(); |
153 | return; |
154 | } |
155 | |
156 | if (foundHostAdapterPath.isEmpty()) { |
157 | // Cannot find a local adapter |
158 | // Abort any outstanding discoveries |
159 | discoveredDevices.clear(); |
160 | |
161 | error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError; |
162 | errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot find local Bluetooth adapter" ); |
163 | emit q->error(error); |
164 | _q_serviceDiscoveryFinished(); |
165 | |
166 | return; |
167 | } |
168 | } |
169 | |
170 | // ensure we didn't go offline yet |
171 | OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez" ), |
172 | foundHostAdapterPath, QDBusConnection::systemBus()); |
173 | if (!adapter.powered()) { |
174 | discoveredDevices.clear(); |
175 | |
176 | error = QBluetoothServiceDiscoveryAgent::PoweredOffError; |
177 | errorString = QBluetoothServiceDiscoveryAgent::tr("Local device is powered off" ); |
178 | emit q->error(error); |
179 | |
180 | _q_serviceDiscoveryFinished(); |
181 | return; |
182 | } |
183 | |
184 | if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) { |
185 | performMinimalServiceDiscovery(address); |
186 | } else { |
187 | runExternalSdpScan(address, QBluetoothAddress(adapter.address())); |
188 | } |
189 | } |
190 | |
191 | /* Bluez 5 |
192 | * src/tools/sdpscanner performs an SDP scan. This is |
193 | * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2. |
194 | */ |
195 | void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan( |
196 | const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress) |
197 | { |
198 | Q_Q(QBluetoothServiceDiscoveryAgent); |
199 | |
200 | if (!sdpScannerProcess) { |
201 | const QString binPath = QLibraryInfo::location(QLibraryInfo::BinariesPath); |
202 | QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner" )); |
203 | if (!fileInfo.exists() || !fileInfo.isExecutable()) { |
204 | _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError, |
205 | QBluetoothServiceDiscoveryAgent::tr("Unable to find sdpscanner" ), |
206 | QStringList()); |
207 | qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:" |
208 | << fileInfo.canonicalFilePath(); |
209 | return; |
210 | } |
211 | |
212 | sdpScannerProcess = new QProcess(q); |
213 | sdpScannerProcess->setReadChannel(QProcess::StandardOutput); |
214 | if (QT_BT_BLUEZ().isDebugEnabled()) |
215 | sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); |
216 | sdpScannerProcess->setProgram(fileInfo.canonicalFilePath()); |
217 | q->connect(sdpScannerProcess, |
218 | QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
219 | q, [this](int exitCode, QProcess::ExitStatus status){ |
220 | this->_q_sdpScannerDone(exitCode, status); |
221 | }); |
222 | } |
223 | |
224 | QStringList arguments; |
225 | arguments << remoteAddress.toString() << localAddress.toString(); |
226 | |
227 | // No filter implies PUBLIC_BROWSE_GROUP based SDP scan |
228 | if (!uuidFilter.isEmpty()) { |
229 | arguments << QLatin1String("-u" ); // cmd line option for list of uuids |
230 | for (const QBluetoothUuid& uuid : qAsConst(uuidFilter)) |
231 | arguments << uuid.toString(); |
232 | } |
233 | |
234 | sdpScannerProcess->setArguments(arguments); |
235 | sdpScannerProcess->start(); |
236 | } |
237 | |
238 | // Bluez 5 |
239 | void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status) |
240 | { |
241 | if (status != QProcess::NormalExit || exitCode != 0) { |
242 | qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode; |
243 | if (singleDevice) { |
244 | _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError, |
245 | QBluetoothServiceDiscoveryAgent::tr("Unable to perform SDP scan" ), |
246 | QStringList()); |
247 | } else { |
248 | // go to next device |
249 | _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), QStringList()); |
250 | } |
251 | return; |
252 | } |
253 | |
254 | QStringList xmlRecords; |
255 | const QByteArray output = sdpScannerProcess->readAllStandardOutput(); |
256 | const QString decodedData = QString::fromUtf8(QByteArray::fromBase64(output)); |
257 | |
258 | // split the various xml docs up |
259 | int next; |
260 | int start = decodedData.indexOf(QStringLiteral("<?xml" ), 0); |
261 | if (start != -1) { |
262 | do { |
263 | next = decodedData.indexOf(QStringLiteral("<?xml" ), start + 1); |
264 | if (next != -1) |
265 | xmlRecords.append(decodedData.mid(start, next-start)); |
266 | else |
267 | xmlRecords.append(decodedData.mid(start, decodedData.size() - start)); |
268 | start = next; |
269 | } while ( start != -1); |
270 | } |
271 | |
272 | _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), xmlRecords); |
273 | } |
274 | |
275 | // Bluez 5 |
276 | void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode, |
277 | const QString &errorDescription, |
278 | const QStringList &xmlRecords) |
279 | { |
280 | Q_Q(QBluetoothServiceDiscoveryAgent); |
281 | |
282 | if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) { |
283 | qCWarning(QT_BT_BLUEZ) << "SDP search failed for" |
284 | << (!discoveredDevices.isEmpty() |
285 | ? discoveredDevices.at(0).address().toString() |
286 | : QStringLiteral("<Unknown>" )); |
287 | // We have an error which we need to indicate and stop further processing |
288 | discoveredDevices.clear(); |
289 | error = errorCode; |
290 | errorString = errorDescription; |
291 | emit q->error(error); |
292 | } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) { |
293 | for (const QString &record : xmlRecords) { |
294 | QBluetoothServiceInfo serviceInfo = parseServiceXml(record); |
295 | |
296 | //apply uuidFilter |
297 | if (!uuidFilter.isEmpty()) { |
298 | bool serviceNameMatched = uuidFilter.contains(serviceInfo.serviceUuid()); |
299 | bool serviceClassMatched = false; |
300 | const QList<QBluetoothUuid> serviceClassUuids |
301 | = serviceInfo.serviceClassUuids(); |
302 | for (const QBluetoothUuid &id : serviceClassUuids) { |
303 | if (uuidFilter.contains(id)) { |
304 | serviceClassMatched = true; |
305 | break; |
306 | } |
307 | } |
308 | |
309 | if (!serviceNameMatched && !serviceClassMatched) |
310 | continue; |
311 | } |
312 | |
313 | if (!serviceInfo.isValid()) |
314 | continue; |
315 | |
316 | // Bluez sdpscanner declares custom uuids into the service class uuid list. |
317 | // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids() |
318 | // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid |
319 | const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids(); |
320 | for (const QBluetoothUuid &id : serviceClassUuids) { |
321 | if (id.minimumSize() == 16) { |
322 | serviceInfo.setServiceUuid(id); |
323 | serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service" )); |
324 | QBluetoothServiceInfo::Sequence modSeq = |
325 | serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>(); |
326 | modSeq.removeOne(QVariant::fromValue(id)); |
327 | serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, modSeq); |
328 | break; |
329 | } |
330 | } |
331 | |
332 | if (!isDuplicatedService(serviceInfo)) { |
333 | discoveredServices.append(serviceInfo); |
334 | qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
335 | << serviceInfo.serviceName() << serviceInfo.serviceUuid() |
336 | << ">>>" << serviceInfo.serviceClassUuids(); |
337 | |
338 | emit q->serviceDiscovered(serviceInfo); |
339 | } |
340 | } |
341 | } |
342 | |
343 | _q_serviceDiscoveryFinished(); |
344 | } |
345 | |
346 | void QBluetoothServiceDiscoveryAgentPrivate::stop() |
347 | { |
348 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called" ; |
349 | if (device) { |
350 | //we are waiting for _q_discoveredServices() to be called |
351 | // adapter is already 0 |
352 | QDBusPendingReply<> reply = device->CancelDiscovery(); |
353 | reply.waitForFinished(); |
354 | |
355 | device->deleteLater(); |
356 | device = nullptr; |
357 | Q_ASSERT(!adapter); |
358 | } else if (adapter) { |
359 | //we are waiting for _q_createdDevice() to be called |
360 | adapter->deleteLater(); |
361 | adapter = nullptr; |
362 | Q_ASSERT(!device); |
363 | } |
364 | |
365 | |
366 | discoveredDevices.clear(); |
367 | setDiscoveryState(Inactive); |
368 | |
369 | // must happen after discoveredDevices.clear() above to avoid retrigger of next scan |
370 | // while waitForFinished() is waiting |
371 | if (sdpScannerProcess) { // Bluez 5 |
372 | if (sdpScannerProcess->state() != QProcess::NotRunning) { |
373 | sdpScannerProcess->kill(); |
374 | sdpScannerProcess->waitForFinished(); |
375 | } |
376 | } |
377 | |
378 | Q_Q(QBluetoothServiceDiscoveryAgent); |
379 | emit q->canceled(); |
380 | } |
381 | |
382 | void QBluetoothServiceDiscoveryAgentPrivate::_q_foundDevice(QDBusPendingCallWatcher *watcher) |
383 | { |
384 | if (!adapter) { |
385 | watcher->deleteLater(); |
386 | return; |
387 | } |
388 | |
389 | Q_Q(QBluetoothServiceDiscoveryAgent); |
390 | |
391 | const QBluetoothAddress &address = watcher->property("_q_BTaddress" ).value<QBluetoothAddress>(); |
392 | |
393 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "found" << address.toString(); |
394 | |
395 | QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher; |
396 | watcher->deleteLater(); |
397 | if (deviceObjectPath.isError()) { |
398 | if (deviceObjectPath.error().name() != QStringLiteral("org.bluez.Error.DoesNotExist" )) { |
399 | qCDebug(QT_BT_BLUEZ) << "Find device failed Error: " << error << deviceObjectPath.error().name(); |
400 | delete adapter; |
401 | adapter = nullptr; |
402 | if (singleDevice) { |
403 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
404 | errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device" ); |
405 | emit q->error(error); |
406 | } |
407 | _q_serviceDiscoveryFinished(); |
408 | return; |
409 | } |
410 | |
411 | deviceObjectPath = adapter->CreateDevice(address.toString()); |
412 | watcher = new QDBusPendingCallWatcher(deviceObjectPath, q); |
413 | watcher->setProperty("_q_BTaddress" , QVariant::fromValue(address)); |
414 | QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
415 | q, [this](QDBusPendingCallWatcher *watcher){ |
416 | this->_q_createdDevice(watcher); |
417 | }); |
418 | |
419 | return; |
420 | } |
421 | |
422 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path(); |
423 | discoverServices(deviceObjectPath.value().path()); |
424 | } |
425 | |
426 | void QBluetoothServiceDiscoveryAgentPrivate::_q_createdDevice(QDBusPendingCallWatcher *watcher) |
427 | { |
428 | Q_Q(QBluetoothServiceDiscoveryAgent); |
429 | |
430 | if (!adapter) { |
431 | watcher->deleteLater(); |
432 | return; |
433 | } |
434 | |
435 | const QBluetoothAddress &address = watcher->property("_q_BTaddress" ).value<QBluetoothAddress>(); |
436 | |
437 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "created" << address.toString(); |
438 | |
439 | QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher; |
440 | watcher->deleteLater(); |
441 | if (deviceObjectPath.isError()) { |
442 | if (deviceObjectPath.error().name() != QLatin1String("org.bluez.Error.AlreadyExists" )) { |
443 | qCDebug(QT_BT_BLUEZ) << "Create device failed Error: " << error << deviceObjectPath.error().name(); |
444 | delete adapter; |
445 | adapter = nullptr; |
446 | if (singleDevice) { |
447 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
448 | errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device" ); |
449 | emit q->error(error); |
450 | } |
451 | _q_serviceDiscoveryFinished(); |
452 | return; |
453 | } |
454 | } |
455 | |
456 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path(); |
457 | discoverServices(deviceObjectPath.value().path()); |
458 | } |
459 | |
460 | void QBluetoothServiceDiscoveryAgentPrivate::discoverServices(const QString &deviceObjectPath) |
461 | { |
462 | Q_Q(QBluetoothServiceDiscoveryAgent); |
463 | |
464 | device = new OrgBluezDeviceInterface(QStringLiteral("org.bluez" ), |
465 | deviceObjectPath, |
466 | QDBusConnection::systemBus()); |
467 | delete adapter; |
468 | adapter = nullptr; |
469 | |
470 | QVariantMap deviceProperties; |
471 | QString classType; |
472 | QDBusPendingReply<QVariantMap> deviceReply = device->GetProperties(); |
473 | deviceReply.waitForFinished(); |
474 | if (!deviceReply.isError()) { |
475 | deviceProperties = deviceReply.value(); |
476 | classType = deviceProperties.value(QStringLiteral("Class" )).toString(); |
477 | } |
478 | |
479 | /* |
480 | * Low Energy services in bluez are represented as the list of the paths that can be |
481 | * accessed with org.bluez.Characteristic |
482 | */ |
483 | //QDBusArgument services = v.value(QLatin1String("Services")).value<QDBusArgument>(); |
484 | |
485 | |
486 | /* |
487 | * Bluez v4.1 does not have extra bit which gives information if device is Bluetooth |
488 | * Low Energy device and the way to discover it is with Class property of the Bluetooth device. |
489 | * Low Energy devices do not have property Class. |
490 | * In case we have LE device finish service discovery; otherwise search for regular services. |
491 | */ |
492 | if (classType.isEmpty()) { //is BLE device or device properties above not retrievable |
493 | qCDebug(QT_BT_BLUEZ) << "Discovered BLE-only device. Normal service discovery skipped." ; |
494 | delete device; |
495 | device = nullptr; |
496 | |
497 | const QStringList deviceUuids = deviceProperties.value(QStringLiteral("UUIDs" )).toStringList(); |
498 | for (int i = 0; i < deviceUuids.size(); i++) { |
499 | QString b = deviceUuids.at(i); |
500 | b = b.remove(QLatin1Char('{')).remove(QLatin1Char('}')); |
501 | const QBluetoothUuid uuid(b); |
502 | |
503 | qCDebug(QT_BT_BLUEZ) << "Discovered service" << uuid << uuidFilter.size(); |
504 | QBluetoothServiceInfo service; |
505 | service.setDevice(discoveredDevices.at(0)); |
506 | bool ok = false; |
507 | quint16 serviceClass = uuid.toUInt16(&ok); |
508 | if (ok) |
509 | service.setServiceName(QBluetoothUuid::serviceClassToString( |
510 | static_cast<QBluetoothUuid::ServiceClassUuid>(serviceClass))); |
511 | |
512 | QBluetoothServiceInfo::Sequence classId; |
513 | classId << QVariant::fromValue(uuid); |
514 | service.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); |
515 | |
516 | QBluetoothServiceInfo::Sequence protocolDescriptorList; |
517 | { |
518 | QBluetoothServiceInfo::Sequence protocol; |
519 | protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)); |
520 | protocolDescriptorList.append(QVariant::fromValue(protocol)); |
521 | } |
522 | { |
523 | QBluetoothServiceInfo::Sequence protocol; |
524 | protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att)); |
525 | protocolDescriptorList.append(QVariant::fromValue(protocol)); |
526 | } |
527 | service.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); |
528 | |
529 | if (uuidFilter.isEmpty()) |
530 | emit q->serviceDiscovered(service); |
531 | else { |
532 | for (int j = 0; j < uuidFilter.size(); j++) { |
533 | if (uuidFilter.at(j) == uuid) |
534 | emit q->serviceDiscovered(service); |
535 | } |
536 | } |
537 | } |
538 | |
539 | if (singleDevice && deviceReply.isError()) { |
540 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
541 | errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device" ); |
542 | emit q->error(error); |
543 | } |
544 | _q_serviceDiscoveryFinished(); |
545 | } else { |
546 | QString pattern; |
547 | for (const QBluetoothUuid &uuid : qAsConst(uuidFilter)) |
548 | pattern += uuid.toString().remove(QLatin1Char('{')).remove(QLatin1Char('}')) + QLatin1Char(' '); |
549 | |
550 | pattern = pattern.trimmed(); |
551 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Discover restrictions:" << pattern; |
552 | |
553 | QDBusPendingReply<ServiceMap> discoverReply = device->DiscoverServices(pattern); |
554 | QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(discoverReply, q); |
555 | QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
556 | q, [this](QDBusPendingCallWatcher *watcher){ |
557 | this->_q_discoveredServices(watcher); |
558 | }); |
559 | } |
560 | } |
561 | |
562 | // Bluez 4 |
563 | void QBluetoothServiceDiscoveryAgentPrivate::_q_discoveredServices(QDBusPendingCallWatcher *watcher) |
564 | { |
565 | if (!device) { |
566 | watcher->deleteLater(); |
567 | return; |
568 | } |
569 | |
570 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO; |
571 | Q_Q(QBluetoothServiceDiscoveryAgent); |
572 | |
573 | QDBusPendingReply<ServiceMap> reply = *watcher; |
574 | if (reply.isError()) { |
575 | qCDebug(QT_BT_BLUEZ) << "discoveredServices error: " << error << reply.error().message(); |
576 | watcher->deleteLater(); |
577 | if (singleDevice) { |
578 | error = QBluetoothServiceDiscoveryAgent::UnknownError; |
579 | errorString = reply.error().message(); |
580 | emit q->error(error); |
581 | } |
582 | delete device; |
583 | device = nullptr; |
584 | _q_serviceDiscoveryFinished(); |
585 | return; |
586 | } |
587 | |
588 | const ServiceMap map = reply.value(); |
589 | |
590 | qCDebug(QT_BT_BLUEZ) << "Parsing xml" << discoveredDevices.at(0).address().toString() << discoveredDevices.count() << map.count(); |
591 | |
592 | |
593 | |
594 | for (const QString &record : map) { |
595 | QBluetoothServiceInfo serviceInfo = parseServiceXml(record); |
596 | |
597 | if (!serviceInfo.isValid()) |
598 | continue; |
599 | |
600 | // Don't need to apply uuidFilter because Bluez 4 applies |
601 | // search pattern during DiscoverServices() call |
602 | |
603 | Q_Q(QBluetoothServiceDiscoveryAgent); |
604 | // Some service uuids are unknown to Bluez. In such cases we fall back |
605 | // to our own naming resolution. |
606 | if (serviceInfo.serviceName().isEmpty() |
607 | && !serviceInfo.serviceClassUuids().isEmpty()) { |
608 | const QList<QBluetoothUuid> classUuids = serviceInfo.serviceClassUuids(); |
609 | for (const QBluetoothUuid &classUuid : classUuids) { |
610 | bool ok = false; |
611 | QBluetoothUuid::ServiceClassUuid clsId |
612 | = static_cast<QBluetoothUuid::ServiceClassUuid>(classUuid.toUInt16(&ok)); |
613 | if (ok) { |
614 | serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId)); |
615 | break; |
616 | } |
617 | } |
618 | } |
619 | |
620 | if (!isDuplicatedService(serviceInfo)) { |
621 | discoveredServices.append(serviceInfo); |
622 | qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
623 | << serviceInfo.serviceName(); |
624 | emit q->serviceDiscovered(serviceInfo); |
625 | } |
626 | |
627 | // could stop discovery, check for state |
628 | if (discoveryState() == Inactive) |
629 | qCDebug(QT_BT_BLUEZ) << "Exit discovery after stop" ; |
630 | } |
631 | |
632 | watcher->deleteLater(); |
633 | delete device; |
634 | device = nullptr; |
635 | |
636 | _q_serviceDiscoveryFinished(); |
637 | } |
638 | |
639 | QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml( |
640 | const QString& xmlRecord) |
641 | { |
642 | QXmlStreamReader xml(xmlRecord); |
643 | |
644 | QBluetoothServiceInfo serviceInfo; |
645 | serviceInfo.setDevice(discoveredDevices.at(0)); |
646 | |
647 | while (!xml.atEnd()) { |
648 | xml.readNext(); |
649 | |
650 | if (xml.tokenType() == QXmlStreamReader::StartElement && |
651 | xml.name() == QLatin1String("attribute" )) { |
652 | quint16 attributeId = |
653 | xml.attributes().value(QLatin1String("id" )).toString().toUShort(nullptr, 0); |
654 | |
655 | if (xml.readNextStartElement()) { |
656 | const QVariant value = readAttributeValue(xml); |
657 | serviceInfo.setAttribute(attributeId, value); |
658 | } |
659 | } |
660 | } |
661 | |
662 | return serviceInfo; |
663 | } |
664 | |
665 | // Bluez 5 |
666 | void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress) |
667 | { |
668 | if (foundHostAdapterPath.isEmpty()) { |
669 | _q_serviceDiscoveryFinished(); |
670 | return; |
671 | } |
672 | |
673 | Q_Q(QBluetoothServiceDiscoveryAgent); |
674 | |
675 | QDBusPendingReply<ManagedObjectList> reply = managerBluez5->GetManagedObjects(); |
676 | reply.waitForFinished(); |
677 | if (reply.isError()) { |
678 | if (singleDevice) { |
679 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
680 | errorString = reply.error().message(); |
681 | emit q->error(error); |
682 | |
683 | } |
684 | _q_serviceDiscoveryFinished(); |
685 | return; |
686 | } |
687 | |
688 | QStringList uuidStrings; |
689 | |
690 | ManagedObjectList managedObjectList = reply.value(); |
691 | for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) { |
692 | const InterfaceList &ifaceList = it.value(); |
693 | |
694 | for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) { |
695 | const QString &iface = jt.key(); |
696 | const QVariantMap &ifaceValues = jt.value(); |
697 | |
698 | if (iface == QStringLiteral("org.bluez.Device1" )) { |
699 | if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address" )).toString()) { |
700 | uuidStrings = ifaceValues.value(QStringLiteral("UUIDs" )).toStringList(); |
701 | break; |
702 | } |
703 | } |
704 | } |
705 | if (!uuidStrings.isEmpty()) |
706 | break; |
707 | } |
708 | |
709 | if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) { |
710 | qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString(); |
711 | // nothing found -> go to next uuid |
712 | _q_serviceDiscoveryFinished(); |
713 | return; |
714 | } |
715 | |
716 | qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings; |
717 | |
718 | QBluetoothUuid uuid; |
719 | for (int i = 0; i < uuidStrings.count(); i++) { |
720 | uuid = QBluetoothUuid(uuidStrings.at(i)); |
721 | if (uuid.isNull()) |
722 | continue; |
723 | |
724 | //apply uuidFilter |
725 | if (!uuidFilter.isEmpty() && !uuidFilter.contains(uuid)) |
726 | continue; |
727 | |
728 | QBluetoothServiceInfo serviceInfo; |
729 | serviceInfo.setDevice(discoveredDevices.at(0)); |
730 | |
731 | if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID |
732 | serviceInfo.setServiceUuid(uuid); |
733 | serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service" )); |
734 | } else { |
735 | // set uuid as service class id |
736 | QBluetoothServiceInfo::Sequence classId; |
737 | classId << QVariant::fromValue(uuid); |
738 | serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); |
739 | QBluetoothUuid::ServiceClassUuid clsId |
740 | = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff); |
741 | serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId)); |
742 | } |
743 | |
744 | QBluetoothServiceInfo::Sequence protocolDescriptorList; |
745 | { |
746 | QBluetoothServiceInfo::Sequence protocol; |
747 | protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)); |
748 | protocolDescriptorList.append(QVariant::fromValue(protocol)); |
749 | } |
750 | { |
751 | QBluetoothServiceInfo::Sequence protocol; |
752 | protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att)); |
753 | protocolDescriptorList.append(QVariant::fromValue(protocol)); |
754 | } |
755 | serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); |
756 | |
757 | //don't include the service if we already discovered it before |
758 | if (!isDuplicatedService(serviceInfo)) { |
759 | discoveredServices << serviceInfo; |
760 | qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
761 | << serviceInfo.serviceName(); |
762 | emit q->serviceDiscovered(serviceInfo); |
763 | } |
764 | } |
765 | |
766 | _q_serviceDiscoveryFinished(); |
767 | } |
768 | |
769 | QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml) |
770 | { |
771 | if (xml.name() == QLatin1String("boolean" )) { |
772 | const QString value = xml.attributes().value(QStringLiteral("value" )).toString(); |
773 | xml.skipCurrentElement(); |
774 | return value == QLatin1String("true" ); |
775 | } else if (xml.name() == QLatin1String("uint8" )) { |
776 | quint8 value = xml.attributes().value(QStringLiteral("value" )).toString().toUShort(nullptr, 0); |
777 | xml.skipCurrentElement(); |
778 | return value; |
779 | } else if (xml.name() == QLatin1String("uint16" )) { |
780 | quint16 value = xml.attributes().value(QStringLiteral("value" )).toString().toUShort(nullptr, 0); |
781 | xml.skipCurrentElement(); |
782 | return value; |
783 | } else if (xml.name() == QLatin1String("uint32" )) { |
784 | quint32 value = xml.attributes().value(QStringLiteral("value" )).toString().toUInt(nullptr, 0); |
785 | xml.skipCurrentElement(); |
786 | return value; |
787 | } else if (xml.name() == QLatin1String("uint64" )) { |
788 | quint64 value = xml.attributes().value(QStringLiteral("value" )).toString().toULongLong(nullptr, 0); |
789 | xml.skipCurrentElement(); |
790 | return value; |
791 | } else if (xml.name() == QLatin1String("uuid" )) { |
792 | QBluetoothUuid uuid; |
793 | const QString value = xml.attributes().value(QStringLiteral("value" )).toString(); |
794 | if (value.startsWith(QStringLiteral("0x" ))) { |
795 | if (value.length() == 6) { |
796 | quint16 v = value.toUShort(nullptr, 0); |
797 | uuid = QBluetoothUuid(v); |
798 | } else if (value.length() == 10) { |
799 | quint32 v = value.toUInt(nullptr, 0); |
800 | uuid = QBluetoothUuid(v); |
801 | } |
802 | } else { |
803 | uuid = QBluetoothUuid(value); |
804 | } |
805 | xml.skipCurrentElement(); |
806 | return QVariant::fromValue(uuid); |
807 | } else if (xml.name() == QLatin1String("text" ) || xml.name() == QLatin1String("url" )) { |
808 | QString value = xml.attributes().value(QStringLiteral("value" )).toString(); |
809 | if (xml.attributes().value(QStringLiteral("encoding" )) == QLatin1String("hex" )) |
810 | value = QString::fromUtf8(QByteArray::fromHex(value.toLatin1())); |
811 | xml.skipCurrentElement(); |
812 | return value; |
813 | } else if (xml.name() == QLatin1String("sequence" )) { |
814 | QBluetoothServiceInfo::Sequence sequence; |
815 | |
816 | while (xml.readNextStartElement()) { |
817 | QVariant value = readAttributeValue(xml); |
818 | sequence.append(value); |
819 | } |
820 | |
821 | return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(sequence); |
822 | } else { |
823 | qCWarning(QT_BT_BLUEZ) << "unknown attribute type" |
824 | << xml.name().toString() |
825 | << xml.attributes().value(QStringLiteral("value" )).toString(); |
826 | xml.skipCurrentElement(); |
827 | return QVariant(); |
828 | } |
829 | } |
830 | |
831 | QT_END_NAMESPACE |
832 | |