Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include <QtCore/QLoggingCategory>
++
++ #include <QtCore/qcoreapplication.h>
++
++ #include "qbluetoothdevicediscoveryagent.h"
++ #include "qbluetoothdevicediscoveryagent_p.h"
++ #include "qbluetoothaddress.h"
++ #include "qbluetoothuuid.h"
++
++ #include "bluez/bluez5_helper_p.h"
++ #include "bluez/objectmanager_p.h"
++ #include "bluez/adapter1_bluez5_p.h"
++ #include "bluez/device1_bluez5_p.h"
++ #include "bluez/properties_p.h"
++ #include "bluez/bluetoothmanagement_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
++
++ using namespace QtBluetoothPrivate; // for D-Bus wrappers
++
++ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(
++ const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
++ adapterAddress(deviceAdapter),
++ q_ptr(parent)
++ {
++ initializeBluez5();
++ manager = new OrgFreedesktopDBusObjectManagerInterface(
++ QStringLiteral("org.bluez"),
++ QStringLiteral("/"),
++ QDBusConnection::systemBus(), parent);
++ QObject::connect(manager,
++ &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded,
++ q_ptr,
++ [this](const QDBusObjectPath &objectPath, InterfaceList interfacesAndProperties) {
++ this->_q_InterfacesAdded(objectPath, interfacesAndProperties);
++ });
++
++ // start private address monitoring
++ BluetoothManagement::instance();
++ }
++
++ QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
++ {
++ delete adapter;
++ }
++
++ //TODO: Qt6 remove the pendingCancel/pendingStart logic as it is cumbersome.
++ // It is a behavior change across all platforms and was initially done
++ // for Bluez. The behavior should be similar to QBluetoothServiceDiscoveryAgent
++ // PendingCancel creates issues whereby the agent is still shutting down
++ // but isActive() below already returns false. This means the isActive() is
++ // out of sync with the finished() and cancel() signal.
++
++ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
++ {
++ if (pendingStart)
++ return true;
++ if (pendingCancel)
++ return false; //TODO Qt6: remove pending[Cancel|Start] logic (see comment above)
++
++ return adapter;
++ }
++
++ QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
++ {
++ return (ClassicMethod | LowEnergyMethod);
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
++ {
++ if (pendingCancel == true) {
++ pendingStart = true;
++ return;
++ }
++
++ lastError = QBluetoothDeviceDiscoveryAgent::NoError;
++ errorString.clear();
++ discoveredDevices.clear();
++ devicesProperties.clear();
++
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++
++ bool ok = false;
++ const QString adapterPath = findAdapterForAddress(adapterAddress, &ok);
++ if (!ok || adapterPath.isEmpty()) {
++ qCWarning(QT_BT_BLUEZ) << "Cannot find Bluez 5 adapter for device search" << ok;
++ lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
++ errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot find valid Bluetooth adapter.");
++ q->errorOccurred(lastError);
++ return;
++ }
++
++ adapter = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"), adapterPath,
++ QDBusConnection::systemBus());
++
++ if (!adapter->powered()) {
++ qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter";
++ lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
++ errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
++ delete adapter;
++ adapter = nullptr;
++ emit q->errorOccurred(lastError);
++ return;
++ }
++
++ QVariantMap map;
++ if (methods == (QBluetoothDeviceDiscoveryAgent::LowEnergyMethod|QBluetoothDeviceDiscoveryAgent::ClassicMethod))
++ map.insert(QStringLiteral("Transport"), QStringLiteral("auto"));
++ else if (methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)
++ map.insert(QStringLiteral("Transport"), QStringLiteral("le"));
++ else
++ map.insert(QStringLiteral("Transport"), QStringLiteral("bredr"));
++
++ // older BlueZ 5.x versions don't have this function
++ // filterReply returns UnknownMethod which we ignore
++ QDBusPendingReply<> filterReply = adapter->SetDiscoveryFilter(map);
++ filterReply.waitForFinished();
++ if (filterReply.isError()) {
++ if (filterReply.error().type() == QDBusError::Other
++ && filterReply.error().name() == QStringLiteral("org.bluez.Error.Failed")) {
++ qCDebug(QT_BT_BLUEZ) << "Discovery method" << methods << "not supported";
++ lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod;
++ errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods "
++ "are not supported on this platform");
++ delete adapter;
++ adapter = nullptr;
++ emit q->errorOccurred(lastError);
++ return;
++ } else if (filterReply.error().type() != QDBusError::UnknownMethod) {
++ qCDebug(QT_BT_BLUEZ) << "SetDiscoveryFilter failed:" << filterReply.error();
++ }
++ }
++
++ QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapter->path());
++ QObject::connect(QtBluezDiscoveryManager::instance(), &QtBluezDiscoveryManager::discoveryInterrupted,
++ q, [this](const QString &path){
++ this->_q_discoveryInterrupted(path);
++ });
++ OrgFreedesktopDBusPropertiesInterface *prop = new OrgFreedesktopDBusPropertiesInterface(
++ QStringLiteral("org.bluez"), QStringLiteral(""), QDBusConnection::systemBus());
++ QObject::connect(prop, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
++ q, [this](const QString &interface, const QVariantMap &changedProperties,
++ const QStringList &invalidatedProperties,
++ const QDBusMessage &signal) {
++ this->_q_PropertiesChanged(interface, signal.path(), changedProperties, invalidatedProperties);
++ });
++
++ // remember what we have to cleanup
++ propertyMonitors.append(prop);
++
++ // collect initial set of information
++ QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects();
++ reply.waitForFinished();
++ if (!reply.isError()) {
++ ManagedObjectList managedObjectList = reply.value();
++ for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
++ const QDBusObjectPath &path = it.key();
++ const InterfaceList &ifaceList = it.value();
++
++ for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
++ const QString &iface = jt.key();
++
++ if (iface == QStringLiteral("org.bluez.Device1")) {
++
++ if (path.path().indexOf(adapter->path()) != 0)
++ continue; //devices whose path doesn't start with same path we skip
++
++ deviceFound(path.path(), jt.value());
++ if (!isActive()) // Can happen if stop() was called from a slot in user code.
++ return;
++ }
++ }
++ }
++ }
++
++ // wait interval and sum up what was found
++ if (!discoveryTimer) {
++ discoveryTimer = new QTimer(q);
++ discoveryTimer->setSingleShot(true);
++ QObject::connect(discoveryTimer, &QTimer::timeout,
++ q, [this]() {
++ this->_q_discoveryFinished();
++ });
++ }
++
++ if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
++ discoveryTimer->setInterval(lowEnergySearchTimeout);
++ discoveryTimer->start();
++ }
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::stop()
++ {
++ if (!adapter)
++ return;
++
++ qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO;
++ pendingCancel = true;
++ pendingStart = false;
++ _q_discoveryFinished();
++ }
++
++ // Returns invalid QBluetoothDeviceInfo in case of error
++ static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap& properties)
++ {
++ const QBluetoothAddress btAddress(properties[QStringLiteral("Address")].toString());
++ if (btAddress.isNull())
++ return QBluetoothDeviceInfo();
++
++ const QString btName = properties[QStringLiteral("Alias")].toString();
++ quint32 btClass = properties[QStringLiteral("Class")].toUInt();
++
++ QBluetoothDeviceInfo deviceInfo(btAddress, btName, btClass);
++ deviceInfo.setRssi(qvariant_cast<short>(properties[QStringLiteral("RSSI")]));
++
++ QList<QBluetoothUuid> uuids;
++ bool foundLikelyLowEnergyUuid = false;
++ const QStringList foundUuids = qvariant_cast<QStringList>(properties[QStringLiteral("UUIDs")]);
++ for (const auto &u: foundUuids) {
++ const QBluetoothUuid id(u);
++ if (id.isNull())
++ continue;
++
++ if (!foundLikelyLowEnergyUuid) {
++ //once we found one BTLE service we are done
++ bool ok = false;
++ quint16 shortId = id.toUInt16(&ok);
++ quint16 genericAccessInt = static_cast<quint16>(QBluetoothUuid::ServiceClassUuid::GenericAccess);
++ if (ok && ((shortId & genericAccessInt) == genericAccessInt))
++ foundLikelyLowEnergyUuid = true;
++ }
++ uuids.append(id);
++ }
++ deviceInfo.setServiceUuids(uuids);
++
++ if (!btClass) {
++ deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
++ } else {
++ deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
++ if (foundLikelyLowEnergyUuid)
++ deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration);
++ }
++
++ const ManufacturerDataList deviceManufacturerData = qdbus_cast<ManufacturerDataList>(properties[QStringLiteral("ManufacturerData")]);
++ for (const auto &[key, value] : deviceManufacturerData.asKeyValueRange())
++ deviceInfo.setManufacturerData(key, value.variant().toByteArray());
++
++ const ServiceDataList deviceServiceData =
++ qdbus_cast<ServiceDataList>(properties[QStringLiteral("ServiceData")]);
++ for (const auto &[key, value] : deviceServiceData.asKeyValueRange())
++ deviceInfo.setServiceData(QBluetoothUuid(key), value.variant().toByteArray());
++
++ return deviceInfo;
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QString &devicePath,
++ const QVariantMap &properties)
++ {
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++
++ if (!q->isActive())
++ return;
++
++ auto deviceAdapter = qvariant_cast<QDBusObjectPath>(properties[QStringLiteral("Adapter")]);
++ if (deviceAdapter.path() != adapter->path())
++ return;
++
++ // read information
++ QBluetoothDeviceInfo deviceInfo = createDeviceInfoFromBluez5Device(properties);
++ if (!deviceInfo.isValid()) // no point reporting an empty address
++ return;
++
++ qCDebug(QT_BT_BLUEZ) << "Discovered: " << deviceInfo.name() << deviceInfo.address()
++ << "Num UUIDs" << deviceInfo.serviceUuids().size()
++ << "total device" << discoveredDevices.size() << "cached"
++ << "RSSI" << deviceInfo.rssi()
++ << "Num ManufacturerData" << deviceInfo.manufacturerData().size()
++ << "Num ServiceData" << deviceInfo.serviceData().size();
++
++ // Cache the properties so we do not have to access dbus every time to get a value
++ devicesProperties[devicePath] = properties;
++
++ for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
++ if (discoveredDevices[i].address() == deviceInfo.address()) {
++ if (lowEnergySearchTimeout > 0 && discoveredDevices[i] == deviceInfo) {
++ qCDebug(QT_BT_BLUEZ) << "Duplicate: " << deviceInfo.address();
++ return;
++ }
++ discoveredDevices.replace(i, deviceInfo);
++
++ emit q->deviceDiscovered(deviceInfo);
++ return; // this works if the list doesn't contain duplicates. Don't let it.
++ }
++ }
++
++ discoveredDevices.append(deviceInfo);
++ emit q->deviceDiscovered(deviceInfo);
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::_q_InterfacesAdded(const QDBusObjectPath &object_path,
++ InterfaceList interfaces_and_properties)
++ {
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++
++ if (!q->isActive())
++ return;
++
++ if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) {
++ // device interfaces belonging to different adapter
++ // will be filtered out by deviceFound();
++ deviceFound(object_path.path(),
++ interfaces_and_properties[QStringLiteral("org.bluez.Device1")]);
++ }
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished()
++ {
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++
++ if (discoveryTimer)
++ discoveryTimer->stop();
++
++ QtBluezDiscoveryManager::instance()->disconnect(q);
++ QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path());
++
++ qDeleteAll(propertyMonitors);
++ propertyMonitors.clear();
++
++ delete adapter;
++ adapter = nullptr;
++
++ if (pendingCancel && !pendingStart) {
++ pendingCancel = false;
++ emit q->canceled();
++ } else if (pendingStart) {
++ pendingStart = false;
++ pendingCancel = false;
++ start(QBluetoothDeviceDiscoveryAgent::ClassicMethod
++ | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
++ } else {
++ emit q->finished();
++ }
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryInterrupted(const QString &path)
++ {
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++
++ if (!q->isActive())
++ return;
++
++ if (path == adapter->path()) {
++ qCWarning(QT_BT_BLUEZ) << "Device discovery aborted due to unexpected adapter changes from another process.";
++
++ if (discoveryTimer)
++ discoveryTimer->stop();
++
++ QtBluezDiscoveryManager::instance()->disconnect(q);
++ // no need to call unregisterDiscoveryInterest since QtBluezDiscoveryManager
++ // does this automatically when emitting discoveryInterrupted(QString) signal
++
++ delete adapter;
++ adapter = nullptr;
++
++ errorString = QBluetoothDeviceDiscoveryAgent::tr("Bluetooth adapter error");
++ lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
++ emit q->errorOccurred(lastError);
++ }
++ }
++
++ void QBluetoothDeviceDiscoveryAgentPrivate::_q_PropertiesChanged(const QString &interface,
++ const QString &path,
++ const QVariantMap &changed_properties,
++ const QStringList &invalidated_properties)
++ {
++ Q_Q(QBluetoothDeviceDiscoveryAgent);
++ if (interface != QStringLiteral("org.bluez.Device1"))
++ return;
++
++ if (!devicesProperties.contains(path))
++ return;
++
++ // Update the cached properties before checking changed_properties for RSSI and ManufacturerData
++ // so the cached properties are always up to date.
++ QVariantMap & properties = devicesProperties[path];
++ for (QVariantMap::const_iterator it = changed_properties.constBegin();
++ it != changed_properties.constEnd(); ++it) {
++ properties[it.key()] = it.value();
++ }
++
++ for (const QString & property : invalidated_properties)
++ properties.remove(property);
++
++ const auto info = createDeviceInfoFromBluez5Device(properties);
++ if (!info.isValid())
++ return;
++
++ if (changed_properties.contains(QStringLiteral("RSSI"))
++ || changed_properties.contains(QStringLiteral("ManufacturerData"))) {
++
++ for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
++ if (discoveredDevices[i].address() == info.address()) {
++ QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
++ if (changed_properties.contains(QStringLiteral("RSSI"))) {
++ qCDebug(QT_BT_BLUEZ) << "Updating RSSI for" << info.address()
++ << changed_properties.value(QStringLiteral("RSSI"));
++ discoveredDevices[i].setRssi(
++ changed_properties.value(QStringLiteral("RSSI")).toInt());
++ updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
++ }
++ if (changed_properties.contains(QStringLiteral("ManufacturerData"))) {
++ qCDebug(QT_BT_BLUEZ) << "Updating ManufacturerData for" << info.address();
++ ManufacturerDataList changedManufacturerData =
++ qdbus_cast< ManufacturerDataList >(changed_properties.value(QStringLiteral("ManufacturerData")));
++
++ const QList<quint16> keys = changedManufacturerData.keys();
++ bool wasNewValue = false;
++ for (quint16 key : keys) {
++ bool added = discoveredDevices[i].setManufacturerData(key, changedManufacturerData.value(key).variant().toByteArray());
++ wasNewValue = (wasNewValue || added);
++ }
++
++ if (wasNewValue)
++ updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
++ }
++
++ if (lowEnergySearchTimeout > 0) {
++ if (discoveredDevices[i] != info) { // field other than manufacturer or rssi changed
++ if (discoveredDevices.at(i).name() == info.name()) {
++ qCDebug(QT_BT_BLUEZ) << "Almost Duplicate " << info.address()
++ << info.name() << "- replacing in place";
++ discoveredDevices.replace(i, info);
++ emit q->deviceDiscovered(info);
++ }
++ } else {
++ if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
++ emit q->deviceUpdated(discoveredDevices[i], updatedFields);
++ }
++
++ return;
++ }
++
++ discoveredDevices.replace(i, info);
++ emit q_ptr->deviceDiscovered(discoveredDevices[i]);
++
++ if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
++ emit q->deviceUpdated(discoveredDevices[i], updatedFields);
++ return;
++ }
++ }
++ }
++ }
++ QT_END_NAMESPACE