| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtBluetooth module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qbluetoothservicediscoveryagent.h" |
| #include "qbluetoothservicediscoveryagent_p.h" |
| |
| #include "bluez/manager_p.h" |
| #include "bluez/adapter_p.h" |
| #include "bluez/device_p.h" |
| #include "bluez/bluez5_helper_p.h" |
| #include "bluez/objectmanager_p.h" |
| #include "bluez/adapter1_bluez5_p.h" |
| |
| #include <QtCore/QFile> |
| #include <QtCore/QLibraryInfo> |
| #include <QtCore/QLoggingCategory> |
| #include <QtCore/QProcess> |
| #include <QtDBus/QDBusPendingCallWatcher> |
| #include <QtConcurrent/QtConcurrentRun> |
| |
| QT_BEGIN_NAMESPACE |
| |
| Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
| |
| QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( |
| QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter) |
| : error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive), |
| mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false), |
| q_ptr(qp) |
| { |
| if (isBluez5()) { |
| managerBluez5 = new OrgFreedesktopDBusObjectManagerInterface( |
| QStringLiteral("org.bluez"), QStringLiteral("/"), |
| QDBusConnection::systemBus()); |
| qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>(); |
| } else { |
| qRegisterMetaType<ServiceMap>(); |
| qDBusRegisterMetaType<ServiceMap>(); |
| |
| manager = new OrgBluezManagerInterface(QStringLiteral("org.bluez"), QStringLiteral("/"), |
| QDBusConnection::systemBus()); |
| } |
| } |
| |
| QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() |
| { |
| delete device; |
| delete manager; |
| delete managerBluez5; |
| delete adapter; |
| } |
| |
| void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode(); |
| |
| if (managerBluez5) { |
| startBluez5(address); |
| return; |
| } |
| |
| QDBusPendingReply<QDBusObjectPath> reply; |
| if (m_deviceAdapterAddress.isNull()) |
| reply = manager->DefaultAdapter(); |
| else |
| reply = manager->FindAdapter(m_deviceAdapterAddress.toString()); |
| |
| reply.waitForFinished(); |
| if (reply.isError()) { |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to find appointed local adapter"); |
| emit q->error(error); |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| adapter = new OrgBluezAdapterInterface(QStringLiteral("org.bluez"), reply.value().path(), |
| QDBusConnection::systemBus()); |
| |
| if (m_deviceAdapterAddress.isNull()) { |
| QDBusPendingReply<QVariantMap> reply = adapter->GetProperties(); |
| reply.waitForFinished(); |
| if (!reply.isError()) { |
| const QBluetoothAddress path_address(reply.value().value(QStringLiteral("Address")).toString()); |
| m_deviceAdapterAddress = path_address; |
| } |
| } |
| |
| QDBusPendingReply<QDBusObjectPath> deviceObjectPath = adapter->FindDevice(address.toString()); |
| |
| QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(deviceObjectPath, q); |
| watcher->setProperty("_q_BTaddress", QVariant::fromValue(address)); |
| QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
| q, [this](QDBusPendingCallWatcher *watcher){ |
| this->_q_foundDevice(watcher); |
| }); |
| } |
| |
| // Bluez 5 |
| void QBluetoothServiceDiscoveryAgentPrivate::startBluez5(const QBluetoothAddress &address) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| if (foundHostAdapterPath.isEmpty()) { |
| // check that we match adapter addresses or use first if it wasn't specified |
| |
| bool ok = false; |
| foundHostAdapterPath = findAdapterForAddress(m_deviceAdapterAddress, &ok); |
| if (!ok) { |
| discoveredDevices.clear(); |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot access adapter during service discovery"); |
| emit q->error(error); |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| if (foundHostAdapterPath.isEmpty()) { |
| // Cannot find a local adapter |
| // Abort any outstanding discoveries |
| discoveredDevices.clear(); |
| |
| error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot find local Bluetooth adapter"); |
| emit q->error(error); |
| _q_serviceDiscoveryFinished(); |
| |
| return; |
| } |
| } |
| |
| // ensure we didn't go offline yet |
| OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez"), |
| foundHostAdapterPath, QDBusConnection::systemBus()); |
| if (!adapter.powered()) { |
| discoveredDevices.clear(); |
| |
| error = QBluetoothServiceDiscoveryAgent::PoweredOffError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Local device is powered off"); |
| emit q->error(error); |
| |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) { |
| performMinimalServiceDiscovery(address); |
| } else { |
| runExternalSdpScan(address, QBluetoothAddress(adapter.address())); |
| } |
| } |
| |
| /* Bluez 5 |
| * src/tools/sdpscanner performs an SDP scan. This is |
| * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2. |
| */ |
| void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan( |
| const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| if (!sdpScannerProcess) { |
| const QString binPath = QLibraryInfo::location(QLibraryInfo::BinariesPath); |
| QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner")); |
| if (!fileInfo.exists() || !fileInfo.isExecutable()) { |
| _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError, |
| QBluetoothServiceDiscoveryAgent::tr("Unable to find sdpscanner"), |
| QStringList()); |
| qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:" |
| << fileInfo.canonicalFilePath(); |
| return; |
| } |
| |
| sdpScannerProcess = new QProcess(q); |
| sdpScannerProcess->setReadChannel(QProcess::StandardOutput); |
| if (QT_BT_BLUEZ().isDebugEnabled()) |
| sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); |
| sdpScannerProcess->setProgram(fileInfo.canonicalFilePath()); |
| q->connect(sdpScannerProcess, |
| QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), |
| q, [this](int exitCode, QProcess::ExitStatus status){ |
| this->_q_sdpScannerDone(exitCode, status); |
| }); |
| } |
| |
| QStringList arguments; |
| arguments << remoteAddress.toString() << localAddress.toString(); |
| |
| // No filter implies PUBLIC_BROWSE_GROUP based SDP scan |
| if (!uuidFilter.isEmpty()) { |
| arguments << QLatin1String("-u"); // cmd line option for list of uuids |
| for (const QBluetoothUuid& uuid : qAsConst(uuidFilter)) |
| arguments << uuid.toString(); |
| } |
| |
| sdpScannerProcess->setArguments(arguments); |
| sdpScannerProcess->start(); |
| } |
| |
| // Bluez 5 |
| void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status) |
| { |
| if (status != QProcess::NormalExit || exitCode != 0) { |
| qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode; |
| if (singleDevice) { |
| _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError, |
| QBluetoothServiceDiscoveryAgent::tr("Unable to perform SDP scan"), |
| QStringList()); |
| } else { |
| // go to next device |
| _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), QStringList()); |
| } |
| return; |
| } |
| |
| QStringList xmlRecords; |
| const QByteArray output = sdpScannerProcess->readAllStandardOutput(); |
| const QString decodedData = QString::fromUtf8(QByteArray::fromBase64(output)); |
| |
| // split the various xml docs up |
| int next; |
| int start = decodedData.indexOf(QStringLiteral("<?xml"), 0); |
| if (start != -1) { |
| do { |
| next = decodedData.indexOf(QStringLiteral("<?xml"), start + 1); |
| if (next != -1) |
| xmlRecords.append(decodedData.mid(start, next-start)); |
| else |
| xmlRecords.append(decodedData.mid(start, decodedData.size() - start)); |
| start = next; |
| } while ( start != -1); |
| } |
| |
| _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), xmlRecords); |
| } |
| |
| // Bluez 5 |
| void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode, |
| const QString &errorDescription, |
| const QStringList &xmlRecords) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) { |
| qCWarning(QT_BT_BLUEZ) << "SDP search failed for" |
| << (!discoveredDevices.isEmpty() |
| ? discoveredDevices.at(0).address().toString() |
| : QStringLiteral("<Unknown>")); |
| // We have an error which we need to indicate and stop further processing |
| discoveredDevices.clear(); |
| error = errorCode; |
| errorString = errorDescription; |
| emit q->error(error); |
| } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) { |
| for (const QString &record : xmlRecords) { |
| QBluetoothServiceInfo serviceInfo = parseServiceXml(record); |
| |
| //apply uuidFilter |
| if (!uuidFilter.isEmpty()) { |
| bool serviceNameMatched = uuidFilter.contains(serviceInfo.serviceUuid()); |
| bool serviceClassMatched = false; |
| const QList<QBluetoothUuid> serviceClassUuids |
| = serviceInfo.serviceClassUuids(); |
| for (const QBluetoothUuid &id : serviceClassUuids) { |
| if (uuidFilter.contains(id)) { |
| serviceClassMatched = true; |
| break; |
| } |
| } |
| |
| if (!serviceNameMatched && !serviceClassMatched) |
| continue; |
| } |
| |
| if (!serviceInfo.isValid()) |
| continue; |
| |
| // Bluez sdpscanner declares custom uuids into the service class uuid list. |
| // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids() |
| // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid |
| const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids(); |
| for (const QBluetoothUuid &id : serviceClassUuids) { |
| if (id.minimumSize() == 16) { |
| serviceInfo.setServiceUuid(id); |
| serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service")); |
| QBluetoothServiceInfo::Sequence modSeq = |
| serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>(); |
| modSeq.removeOne(QVariant::fromValue(id)); |
| serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, modSeq); |
| break; |
| } |
| } |
| |
| if (!isDuplicatedService(serviceInfo)) { |
| discoveredServices.append(serviceInfo); |
| qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
| << serviceInfo.serviceName() << serviceInfo.serviceUuid() |
| << ">>>" << serviceInfo.serviceClassUuids(); |
| |
| emit q->serviceDiscovered(serviceInfo); |
| } |
| } |
| } |
| |
| _q_serviceDiscoveryFinished(); |
| } |
| |
| void QBluetoothServiceDiscoveryAgentPrivate::stop() |
| { |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called"; |
| if (device) { |
| //we are waiting for _q_discoveredServices() to be called |
| // adapter is already 0 |
| QDBusPendingReply<> reply = device->CancelDiscovery(); |
| reply.waitForFinished(); |
| |
| device->deleteLater(); |
| device = nullptr; |
| Q_ASSERT(!adapter); |
| } else if (adapter) { |
| //we are waiting for _q_createdDevice() to be called |
| adapter->deleteLater(); |
| adapter = nullptr; |
| Q_ASSERT(!device); |
| } |
| |
| |
| discoveredDevices.clear(); |
| setDiscoveryState(Inactive); |
| |
| // must happen after discoveredDevices.clear() above to avoid retrigger of next scan |
| // while waitForFinished() is waiting |
| if (sdpScannerProcess) { // Bluez 5 |
| if (sdpScannerProcess->state() != QProcess::NotRunning) { |
| sdpScannerProcess->kill(); |
| sdpScannerProcess->waitForFinished(); |
| } |
| } |
| |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| emit q->canceled(); |
| } |
| |
| void QBluetoothServiceDiscoveryAgentPrivate::_q_foundDevice(QDBusPendingCallWatcher *watcher) |
| { |
| if (!adapter) { |
| watcher->deleteLater(); |
| return; |
| } |
| |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| const QBluetoothAddress &address = watcher->property("_q_BTaddress").value<QBluetoothAddress>(); |
| |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "found" << address.toString(); |
| |
| QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher; |
| watcher->deleteLater(); |
| if (deviceObjectPath.isError()) { |
| if (deviceObjectPath.error().name() != QStringLiteral("org.bluez.Error.DoesNotExist")) { |
| qCDebug(QT_BT_BLUEZ) << "Find device failed Error: " << error << deviceObjectPath.error().name(); |
| delete adapter; |
| adapter = nullptr; |
| if (singleDevice) { |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device"); |
| emit q->error(error); |
| } |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| deviceObjectPath = adapter->CreateDevice(address.toString()); |
| watcher = new QDBusPendingCallWatcher(deviceObjectPath, q); |
| watcher->setProperty("_q_BTaddress", QVariant::fromValue(address)); |
| QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
| q, [this](QDBusPendingCallWatcher *watcher){ |
| this->_q_createdDevice(watcher); |
| }); |
| |
| return; |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path(); |
| discoverServices(deviceObjectPath.value().path()); |
| } |
| |
| void QBluetoothServiceDiscoveryAgentPrivate::_q_createdDevice(QDBusPendingCallWatcher *watcher) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| if (!adapter) { |
| watcher->deleteLater(); |
| return; |
| } |
| |
| const QBluetoothAddress &address = watcher->property("_q_BTaddress").value<QBluetoothAddress>(); |
| |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "created" << address.toString(); |
| |
| QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher; |
| watcher->deleteLater(); |
| if (deviceObjectPath.isError()) { |
| if (deviceObjectPath.error().name() != QLatin1String("org.bluez.Error.AlreadyExists")) { |
| qCDebug(QT_BT_BLUEZ) << "Create device failed Error: " << error << deviceObjectPath.error().name(); |
| delete adapter; |
| adapter = nullptr; |
| if (singleDevice) { |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device"); |
| emit q->error(error); |
| } |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path(); |
| discoverServices(deviceObjectPath.value().path()); |
| } |
| |
| void QBluetoothServiceDiscoveryAgentPrivate::discoverServices(const QString &deviceObjectPath) |
| { |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| device = new OrgBluezDeviceInterface(QStringLiteral("org.bluez"), |
| deviceObjectPath, |
| QDBusConnection::systemBus()); |
| delete adapter; |
| adapter = nullptr; |
| |
| QVariantMap deviceProperties; |
| QString classType; |
| QDBusPendingReply<QVariantMap> deviceReply = device->GetProperties(); |
| deviceReply.waitForFinished(); |
| if (!deviceReply.isError()) { |
| deviceProperties = deviceReply.value(); |
| classType = deviceProperties.value(QStringLiteral("Class")).toString(); |
| } |
| |
| /* |
| * Low Energy services in bluez are represented as the list of the paths that can be |
| * accessed with org.bluez.Characteristic |
| */ |
| //QDBusArgument services = v.value(QLatin1String("Services")).value<QDBusArgument>(); |
| |
| |
| /* |
| * Bluez v4.1 does not have extra bit which gives information if device is Bluetooth |
| * Low Energy device and the way to discover it is with Class property of the Bluetooth device. |
| * Low Energy devices do not have property Class. |
| * In case we have LE device finish service discovery; otherwise search for regular services. |
| */ |
| if (classType.isEmpty()) { //is BLE device or device properties above not retrievable |
| qCDebug(QT_BT_BLUEZ) << "Discovered BLE-only device. Normal service discovery skipped."; |
| delete device; |
| device = nullptr; |
| |
| const QStringList deviceUuids = deviceProperties.value(QStringLiteral("UUIDs")).toStringList(); |
| for (int i = 0; i < deviceUuids.size(); i++) { |
| QString b = deviceUuids.at(i); |
| b = b.remove(QLatin1Char('{')).remove(QLatin1Char('}')); |
| const QBluetoothUuid uuid(b); |
| |
| qCDebug(QT_BT_BLUEZ) << "Discovered service" << uuid << uuidFilter.size(); |
| QBluetoothServiceInfo service; |
| service.setDevice(discoveredDevices.at(0)); |
| bool ok = false; |
| quint16 serviceClass = uuid.toUInt16(&ok); |
| if (ok) |
| service.setServiceName(QBluetoothUuid::serviceClassToString( |
| static_cast<QBluetoothUuid::ServiceClassUuid>(serviceClass))); |
| |
| QBluetoothServiceInfo::Sequence classId; |
| classId << QVariant::fromValue(uuid); |
| service.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); |
| |
| QBluetoothServiceInfo::Sequence protocolDescriptorList; |
| { |
| QBluetoothServiceInfo::Sequence protocol; |
| protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)); |
| protocolDescriptorList.append(QVariant::fromValue(protocol)); |
| } |
| { |
| QBluetoothServiceInfo::Sequence protocol; |
| protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att)); |
| protocolDescriptorList.append(QVariant::fromValue(protocol)); |
| } |
| service.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); |
| |
| if (uuidFilter.isEmpty()) |
| emit q->serviceDiscovered(service); |
| else { |
| for (int j = 0; j < uuidFilter.size(); j++) { |
| if (uuidFilter.at(j) == uuid) |
| emit q->serviceDiscovered(service); |
| } |
| } |
| } |
| |
| if (singleDevice && deviceReply.isError()) { |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device"); |
| emit q->error(error); |
| } |
| _q_serviceDiscoveryFinished(); |
| } else { |
| QString pattern; |
| for (const QBluetoothUuid &uuid : qAsConst(uuidFilter)) |
| pattern += uuid.toString().remove(QLatin1Char('{')).remove(QLatin1Char('}')) + QLatin1Char(' '); |
| |
| pattern = pattern.trimmed(); |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Discover restrictions:" << pattern; |
| |
| QDBusPendingReply<ServiceMap> discoverReply = device->DiscoverServices(pattern); |
| QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(discoverReply, q); |
| QObject::connect(watcher, &QDBusPendingCallWatcher::finished, |
| q, [this](QDBusPendingCallWatcher *watcher){ |
| this->_q_discoveredServices(watcher); |
| }); |
| } |
| } |
| |
| // Bluez 4 |
| void QBluetoothServiceDiscoveryAgentPrivate::_q_discoveredServices(QDBusPendingCallWatcher *watcher) |
| { |
| if (!device) { |
| watcher->deleteLater(); |
| return; |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO; |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| QDBusPendingReply<ServiceMap> reply = *watcher; |
| if (reply.isError()) { |
| qCDebug(QT_BT_BLUEZ) << "discoveredServices error: " << error << reply.error().message(); |
| watcher->deleteLater(); |
| if (singleDevice) { |
| error = QBluetoothServiceDiscoveryAgent::UnknownError; |
| errorString = reply.error().message(); |
| emit q->error(error); |
| } |
| delete device; |
| device = nullptr; |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| const ServiceMap map = reply.value(); |
| |
| qCDebug(QT_BT_BLUEZ) << "Parsing xml" << discoveredDevices.at(0).address().toString() << discoveredDevices.count() << map.count(); |
| |
| |
| |
| for (const QString &record : map) { |
| QBluetoothServiceInfo serviceInfo = parseServiceXml(record); |
| |
| if (!serviceInfo.isValid()) |
| continue; |
| |
| // Don't need to apply uuidFilter because Bluez 4 applies |
| // search pattern during DiscoverServices() call |
| |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| // Some service uuids are unknown to Bluez. In such cases we fall back |
| // to our own naming resolution. |
| if (serviceInfo.serviceName().isEmpty() |
| && !serviceInfo.serviceClassUuids().isEmpty()) { |
| const QList<QBluetoothUuid> classUuids = serviceInfo.serviceClassUuids(); |
| for (const QBluetoothUuid &classUuid : classUuids) { |
| bool ok = false; |
| QBluetoothUuid::ServiceClassUuid clsId |
| = static_cast<QBluetoothUuid::ServiceClassUuid>(classUuid.toUInt16(&ok)); |
| if (ok) { |
| serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId)); |
| break; |
| } |
| } |
| } |
| |
| if (!isDuplicatedService(serviceInfo)) { |
| discoveredServices.append(serviceInfo); |
| qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
| << serviceInfo.serviceName(); |
| emit q->serviceDiscovered(serviceInfo); |
| } |
| |
| // could stop discovery, check for state |
| if (discoveryState() == Inactive) |
| qCDebug(QT_BT_BLUEZ) << "Exit discovery after stop"; |
| } |
| |
| watcher->deleteLater(); |
| delete device; |
| device = nullptr; |
| |
| _q_serviceDiscoveryFinished(); |
| } |
| |
| QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml( |
| const QString& xmlRecord) |
| { |
| QXmlStreamReader xml(xmlRecord); |
| |
| QBluetoothServiceInfo serviceInfo; |
| serviceInfo.setDevice(discoveredDevices.at(0)); |
| |
| while (!xml.atEnd()) { |
| xml.readNext(); |
| |
| if (xml.tokenType() == QXmlStreamReader::StartElement && |
| xml.name() == QLatin1String("attribute")) { |
| quint16 attributeId = |
| xml.attributes().value(QLatin1String("id")).toString().toUShort(nullptr, 0); |
| |
| if (xml.readNextStartElement()) { |
| const QVariant value = readAttributeValue(xml); |
| serviceInfo.setAttribute(attributeId, value); |
| } |
| } |
| } |
| |
| return serviceInfo; |
| } |
| |
| // Bluez 5 |
| void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress) |
| { |
| if (foundHostAdapterPath.isEmpty()) { |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| Q_Q(QBluetoothServiceDiscoveryAgent); |
| |
| QDBusPendingReply<ManagedObjectList> reply = managerBluez5->GetManagedObjects(); |
| reply.waitForFinished(); |
| if (reply.isError()) { |
| if (singleDevice) { |
| error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| errorString = reply.error().message(); |
| emit q->error(error); |
| |
| } |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| QStringList uuidStrings; |
| |
| ManagedObjectList managedObjectList = reply.value(); |
| for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) { |
| const InterfaceList &ifaceList = it.value(); |
| |
| for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) { |
| const QString &iface = jt.key(); |
| const QVariantMap &ifaceValues = jt.value(); |
| |
| if (iface == QStringLiteral("org.bluez.Device1")) { |
| if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address")).toString()) { |
| uuidStrings = ifaceValues.value(QStringLiteral("UUIDs")).toStringList(); |
| break; |
| } |
| } |
| } |
| if (!uuidStrings.isEmpty()) |
| break; |
| } |
| |
| if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) { |
| qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString(); |
| // nothing found -> go to next uuid |
| _q_serviceDiscoveryFinished(); |
| return; |
| } |
| |
| qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings; |
| |
| QBluetoothUuid uuid; |
| for (int i = 0; i < uuidStrings.count(); i++) { |
| uuid = QBluetoothUuid(uuidStrings.at(i)); |
| if (uuid.isNull()) |
| continue; |
| |
| //apply uuidFilter |
| if (!uuidFilter.isEmpty() && !uuidFilter.contains(uuid)) |
| continue; |
| |
| QBluetoothServiceInfo serviceInfo; |
| serviceInfo.setDevice(discoveredDevices.at(0)); |
| |
| if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID |
| serviceInfo.setServiceUuid(uuid); |
| serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service")); |
| } else { |
| // set uuid as service class id |
| QBluetoothServiceInfo::Sequence classId; |
| classId << QVariant::fromValue(uuid); |
| serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); |
| QBluetoothUuid::ServiceClassUuid clsId |
| = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff); |
| serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId)); |
| } |
| |
| QBluetoothServiceInfo::Sequence protocolDescriptorList; |
| { |
| QBluetoothServiceInfo::Sequence protocol; |
| protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)); |
| protocolDescriptorList.append(QVariant::fromValue(protocol)); |
| } |
| { |
| QBluetoothServiceInfo::Sequence protocol; |
| protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att)); |
| protocolDescriptorList.append(QVariant::fromValue(protocol)); |
| } |
| serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); |
| |
| //don't include the service if we already discovered it before |
| if (!isDuplicatedService(serviceInfo)) { |
| discoveredServices << serviceInfo; |
| qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() |
| << serviceInfo.serviceName(); |
| emit q->serviceDiscovered(serviceInfo); |
| } |
| } |
| |
| _q_serviceDiscoveryFinished(); |
| } |
| |
| QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml) |
| { |
| if (xml.name() == QLatin1String("boolean")) { |
| const QString value = xml.attributes().value(QStringLiteral("value")).toString(); |
| xml.skipCurrentElement(); |
| return value == QLatin1String("true"); |
| } else if (xml.name() == QLatin1String("uint8")) { |
| quint8 value = xml.attributes().value(QStringLiteral("value")).toString().toUShort(nullptr, 0); |
| xml.skipCurrentElement(); |
| return value; |
| } else if (xml.name() == QLatin1String("uint16")) { |
| quint16 value = xml.attributes().value(QStringLiteral("value")).toString().toUShort(nullptr, 0); |
| xml.skipCurrentElement(); |
| return value; |
| } else if (xml.name() == QLatin1String("uint32")) { |
| quint32 value = xml.attributes().value(QStringLiteral("value")).toString().toUInt(nullptr, 0); |
| xml.skipCurrentElement(); |
| return value; |
| } else if (xml.name() == QLatin1String("uint64")) { |
| quint64 value = xml.attributes().value(QStringLiteral("value")).toString().toULongLong(nullptr, 0); |
| xml.skipCurrentElement(); |
| return value; |
| } else if (xml.name() == QLatin1String("uuid")) { |
| QBluetoothUuid uuid; |
| const QString value = xml.attributes().value(QStringLiteral("value")).toString(); |
| if (value.startsWith(QStringLiteral("0x"))) { |
| if (value.length() == 6) { |
| quint16 v = value.toUShort(nullptr, 0); |
| uuid = QBluetoothUuid(v); |
| } else if (value.length() == 10) { |
| quint32 v = value.toUInt(nullptr, 0); |
| uuid = QBluetoothUuid(v); |
| } |
| } else { |
| uuid = QBluetoothUuid(value); |
| } |
| xml.skipCurrentElement(); |
| return QVariant::fromValue(uuid); |
| } else if (xml.name() == QLatin1String("text") || xml.name() == QLatin1String("url")) { |
| QString value = xml.attributes().value(QStringLiteral("value")).toString(); |
| if (xml.attributes().value(QStringLiteral("encoding")) == QLatin1String("hex")) |
| value = QString::fromUtf8(QByteArray::fromHex(value.toLatin1())); |
| xml.skipCurrentElement(); |
| return value; |
| } else if (xml.name() == QLatin1String("sequence")) { |
| QBluetoothServiceInfo::Sequence sequence; |
| |
| while (xml.readNextStartElement()) { |
| QVariant value = readAttributeValue(xml); |
| sequence.append(value); |
| } |
| |
| return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(sequence); |
| } else { |
| qCWarning(QT_BT_BLUEZ) << "unknown attribute type" |
| << xml.name().toString() |
| << xml.attributes().value(QStringLiteral("value")).toString(); |
| xml.skipCurrentElement(); |
| return QVariant(); |
| } |
| } |
| |
| QT_END_NAMESPACE |