// 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 #include #include #include "qbluetoothlocaldevice.h" #include "qbluetoothaddress.h" #include "qbluetoothlocaldevice_p.h" #include "bluez/bluez5_helper_p.h" #include "bluez/objectmanager_p.h" #include "bluez/properties_p.h" #include "bluez/adapter1_bluez5_p.h" #include "bluez/device1_bluez5_p.h" QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) QBluetoothLocalDevice::QBluetoothLocalDevice(QObject *parent) : QObject(parent), d_ptr(new QBluetoothLocalDevicePrivate(this)) { d_ptr->currentMode = hostMode(); } QBluetoothLocalDevice::QBluetoothLocalDevice(const QBluetoothAddress &address, QObject *parent) : QObject(parent), d_ptr(new QBluetoothLocalDevicePrivate(this, address)) { d_ptr->currentMode = hostMode(); } QString QBluetoothLocalDevice::name() const { if (d_ptr->adapter) return d_ptr->adapter->alias(); return QString(); } QBluetoothAddress QBluetoothLocalDevice::address() const { if (d_ptr->adapter) return QBluetoothAddress(d_ptr->adapter->address()); return QBluetoothAddress(); } void QBluetoothLocalDevice::powerOn() { if (d_ptr->adapter) d_ptr->adapter->setPowered(true); } void QBluetoothLocalDevice::setHostMode(QBluetoothLocalDevice::HostMode mode) { if (!isValid()) return; Q_D(QBluetoothLocalDevice); if (d->pendingHostModeChange != -1) { qCWarning(QT_BT_BLUEZ) << "setHostMode() ignored due to already pending mode change"; return; } switch (mode) { case HostDiscoverableLimitedInquiry: case HostDiscoverable: if (hostMode() == HostPoweredOff) { // We first have to wait for BT to be powered on, // then we can set the host mode correctly d->pendingHostModeChange = static_cast(HostDiscoverable); d->adapter->setPowered(true); } else { d->adapter->setDiscoverable(true); } break; case HostConnectable: if (hostMode() == HostPoweredOff) { d->pendingHostModeChange = static_cast(HostConnectable); d->adapter->setPowered(true); } else { d->adapter->setDiscoverable(false); } break; case HostPoweredOff: d->adapter->setPowered(false); break; } } QBluetoothLocalDevice::HostMode QBluetoothLocalDevice::hostMode() const { if (d_ptr->adapter) { if (!d_ptr->adapter->powered()) return HostPoweredOff; else if (d_ptr->adapter->discoverable()) return HostDiscoverable; else if (d_ptr->adapter->powered()) return HostConnectable; } return HostPoweredOff; } QList QBluetoothLocalDevice::connectedDevices() const { return d_ptr->connectedDevices(); } QList QBluetoothLocalDevice::allDevices() { QList localDevices; initializeBluez5(); OrgFreedesktopDBusObjectManagerInterface manager( QStringLiteral("org.bluez"), QStringLiteral("/"), QDBusConnection::systemBus()); QDBusPendingReply reply = manager.GetManagedObjects(); reply.waitForFinished(); if (reply.isError()) return localDevices; 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.Adapter1")) { QBluetoothHostInfo hostInfo; const QString temp = ifaceValues.value(QStringLiteral("Address")).toString(); hostInfo.setAddress(QBluetoothAddress(temp)); if (hostInfo.address().isNull()) continue; hostInfo.setName(ifaceValues.value(QStringLiteral("Name")).toString()); localDevices.append(hostInfo); } } } return localDevices; } void QBluetoothLocalDevice::requestPairing(const QBluetoothAddress &address, Pairing pairing) { if (!isValid() || address.isNull()) { QMetaObject::invokeMethod(this, "errorOccurred", Qt::QueuedConnection, Q_ARG(QBluetoothLocalDevice::Error, QBluetoothLocalDevice::PairingError)); return; } const Pairing current_pairing = pairingStatus(address); if (current_pairing == pairing) { if (d_ptr->adapter) { // A possibly running discovery or pending pairing request should be canceled if (d_ptr->pairingDiscoveryTimer && d_ptr->pairingDiscoveryTimer->isActive()) { d_ptr->pairingDiscoveryTimer->stop(); } if (d_ptr->pairingTarget) { qCDebug(QT_BT_BLUEZ) << "Cancelling pending pairing request to" << d_ptr->pairingTarget->address(); QDBusPendingReply<> cancelReply = d_ptr->pairingTarget->CancelPairing(); d_ptr->pairingRequestCanceled = true; cancelReply.waitForFinished(); delete d_ptr->pairingTarget; d_ptr->pairingTarget = nullptr; } } QMetaObject::invokeMethod(this, "pairingFinished", Qt::QueuedConnection, Q_ARG(QBluetoothAddress, address), Q_ARG(QBluetoothLocalDevice::Pairing, pairing)); return; } d_ptr->requestPairing(address, pairing); } void QBluetoothLocalDevicePrivate::requestPairing(const QBluetoothAddress &targetAddress, QBluetoothLocalDevice::Pairing targetPairing) { if (!isValid()) return; //are we already discovering something? -> abort those attempts if (pairingDiscoveryTimer && pairingDiscoveryTimer->isActive()) { pairingDiscoveryTimer->stop(); QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path()); } if (pairingTarget) { delete pairingTarget; pairingTarget = nullptr; } // pairing implies that the device was found // if we cannot find it we may have to turn on Discovery mode for a limited amount of time // check device doesn't already exist QDBusPendingReply reply = manager->GetManagedObjects(); reply.waitForFinished(); if (reply.isError()) { emit q_ptr->errorOccurred(QBluetoothLocalDevice::PairingError); return; } 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")) { OrgBluezDevice1Interface device(QStringLiteral("org.bluez"), path.path(), QDBusConnection::systemBus()); if (targetAddress == QBluetoothAddress(device.address())) { qCDebug(QT_BT_BLUEZ) << "Initiating direct pair to" << targetAddress.toString(); //device exist -> directly work with it processPairing(path.path(), targetPairing); return; } } } } //no device matching -> turn on discovery QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapter->path()); address = targetAddress; pairing = targetPairing; if (!pairingDiscoveryTimer) { pairingDiscoveryTimer = new QTimer(this); pairingDiscoveryTimer->setSingleShot(true); pairingDiscoveryTimer->setInterval(20000); //20s connect(pairingDiscoveryTimer, &QTimer::timeout, this, &QBluetoothLocalDevicePrivate::pairingDiscoveryTimedOut); } qCDebug(QT_BT_BLUEZ) << "Initiating discovery for pairing on" << targetAddress.toString(); pairingDiscoveryTimer->start(); } /*! * \internal * * Found a matching device. Now we must ensure its pairing/trusted state is as desired. * If it has to be paired then we need another roundtrip through the event loop * while we wait for the user to accept the pairing dialogs. */ void QBluetoothLocalDevicePrivate::processPairing(const QString &objectPath, QBluetoothLocalDevice::Pairing target) { if (pairingTarget) delete pairingTarget; //stop possibly running discovery if (pairingDiscoveryTimer && pairingDiscoveryTimer->isActive()) { pairingDiscoveryTimer->stop(); QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path()); } pairingTarget = new OrgBluezDevice1Interface(QStringLiteral("org.bluez"), objectPath, QDBusConnection::systemBus(), this); const QBluetoothAddress targetAddress(pairingTarget->address()); Q_Q(QBluetoothLocalDevice); switch (target) { case QBluetoothLocalDevice::Unpaired: { delete pairingTarget; pairingTarget = nullptr; QDBusPendingReply<> removeReply = adapter->RemoveDevice(QDBusObjectPath(objectPath)); auto watcher = new QDBusPendingCallWatcher(removeReply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [q, targetAddress](QDBusPendingCallWatcher* watcher){ QDBusPendingReply<> reply = *watcher; if (reply.isError()) emit q->errorOccurred(QBluetoothLocalDevice::PairingError); else emit q->pairingFinished(targetAddress, QBluetoothLocalDevice::Unpaired); watcher->deleteLater(); }); break; } case QBluetoothLocalDevice::Paired: case QBluetoothLocalDevice::AuthorizedPaired: pairing = target; if (!pairingTarget->paired()) { qCDebug(QT_BT_BLUEZ) << "Sending pairing request to" << pairingTarget->address(); //initiate the pairing QDBusPendingReply<> pairReply = pairingTarget->Pair(); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pairReply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, &QBluetoothLocalDevicePrivate::pairingCompleted); return; } //already paired but Trust level must be adjusted if (target == QBluetoothLocalDevice::AuthorizedPaired && !pairingTarget->trusted()) pairingTarget->setTrusted(true); else if (target == QBluetoothLocalDevice::Paired && pairingTarget->trusted()) pairingTarget->setTrusted(false); delete pairingTarget; pairingTarget = nullptr; emit q->pairingFinished(targetAddress, target); break; default: break; } } void QBluetoothLocalDevicePrivate::pairingDiscoveryTimedOut() { qCWarning(QT_BT_BLUEZ) << "Discovery for pairing purposes failed. Cannot find parable device."; QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapter->path()); emit q_ptr->errorOccurred(QBluetoothLocalDevice::PairingError); } QBluetoothLocalDevice::Pairing QBluetoothLocalDevice::pairingStatus( const QBluetoothAddress &address) const { if (address.isNull()) return Unpaired; if (isValid()) { QDBusPendingReply reply = d_ptr->manager->GetManagedObjects(); reply.waitForFinished(); if (reply.isError()) return Unpaired; 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")) { OrgBluezDevice1Interface device(QStringLiteral("org.bluez"), path.path(), QDBusConnection::systemBus()); if (address == QBluetoothAddress(device.address())) { if (device.trusted() && device.paired()) return AuthorizedPaired; else if (device.paired()) return Paired; else return Unpaired; } } } } } return Unpaired; } QBluetoothLocalDevicePrivate::QBluetoothLocalDevicePrivate(QBluetoothLocalDevice *q, QBluetoothAddress address) : localAddress(address), pendingHostModeChange(-1), q_ptr(q) { registerQBluetoothLocalDeviceMetaType(); initializeBluez5(); initializeAdapter(); connectDeviceChanges(); } bool objectPathIsForThisDevice(const QString &adapterPath, const QString &objectPath) { return (!adapterPath.isEmpty() && objectPath.startsWith(adapterPath)); } void QBluetoothLocalDevicePrivate::connectDeviceChanges() { if (isValid()) { //setup property change notifications for all existing devices QDBusPendingReply reply = manager->GetManagedObjects(); reply.waitForFinished(); if (reply.isError()) return; OrgFreedesktopDBusPropertiesInterface *monitor = nullptr; 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(); // don't track connected devices from other adapters but the current if (!objectPathIsForThisDevice(deviceAdapterPath, path.path())) continue; 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")) { monitor = new OrgFreedesktopDBusPropertiesInterface(QStringLiteral("org.bluez"), path.path(), QDBusConnection::systemBus(), this); connect(monitor, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &QBluetoothLocalDevicePrivate::PropertiesChanged); deviceChangeMonitors.insert(path.path(), monitor); if (ifaceValues.value(QStringLiteral("Connected"), false).toBool()) { QBluetoothAddress address(ifaceValues.value(QStringLiteral("Address")).toString()); connectedDevicesSet.insert(address); } } } } } } QBluetoothLocalDevicePrivate::~QBluetoothLocalDevicePrivate() { delete adapter; delete adapterProperties; delete manager; delete pairingTarget; qDeleteAll(deviceChangeMonitors); } void QBluetoothLocalDevicePrivate::initializeAdapter() { if (adapter) return; //get all local adapters if (!manager) manager = new OrgFreedesktopDBusObjectManagerInterface( QStringLiteral("org.bluez"), QStringLiteral("/"), QDBusConnection::systemBus(), this); connect(manager, &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded, this, &QBluetoothLocalDevicePrivate::InterfacesAdded); connect(manager, &OrgFreedesktopDBusObjectManagerInterface::InterfacesRemoved, this, &QBluetoothLocalDevicePrivate::InterfacesRemoved); bool ok = true; const QString adapterPath = findAdapterForAddress(localAddress, &ok); if (!ok || adapterPath.isEmpty()) return; deviceAdapterPath = adapterPath; adapter = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus(), this); if (adapter) { //hook up propertiesChanged for current adapter adapterProperties = new OrgFreedesktopDBusPropertiesInterface( QStringLiteral("org.bluez"), adapter->path(), QDBusConnection::systemBus(), this); connect(adapterProperties, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &QBluetoothLocalDevicePrivate::PropertiesChanged); } } void QBluetoothLocalDevicePrivate::PropertiesChanged(const QString &interface, const QVariantMap &changed_properties, const QStringList &/*invalidated_properties*/, const QDBusMessage &) { //qDebug() << "Change" << interface << changed_properties; if (interface == QStringLiteral("org.bluez.Adapter1")) { //update host mode if (changed_properties.contains(QStringLiteral("Discoverable")) || changed_properties.contains(QStringLiteral("Powered"))) { QBluetoothLocalDevice::HostMode mode; if (!adapter->powered()) { mode = QBluetoothLocalDevice::HostPoweredOff; } else { if (adapter->discoverable()) mode = QBluetoothLocalDevice::HostDiscoverable; else mode = QBluetoothLocalDevice::HostConnectable; if (pendingHostModeChange != -1) { if (static_cast(mode) != pendingHostModeChange) { adapter->setDiscoverable( pendingHostModeChange == static_cast(QBluetoothLocalDevice::HostDiscoverable)); pendingHostModeChange = -1; return; } pendingHostModeChange = -1; } } if (mode != currentMode) emit q_ptr->hostModeStateChanged(mode); currentMode = mode; } } else if (interface == QStringLiteral("org.bluez.Device1") && changed_properties.contains(QStringLiteral("Connected"))) { // update list of connected devices OrgFreedesktopDBusPropertiesInterface *senderIface = qobject_cast(sender()); if (!senderIface) return; const QString currentPath = senderIface->path(); bool isConnected = changed_properties.value(QStringLiteral("Connected"), false).toBool(); OrgBluezDevice1Interface device(QStringLiteral("org.bluez"), currentPath, QDBusConnection::systemBus()); const QBluetoothAddress changedAddress(device.address()); bool isInSet = connectedDevicesSet.contains(changedAddress); if (isConnected && !isInSet) { connectedDevicesSet.insert(changedAddress); emit q_ptr->deviceConnected(changedAddress); } else if (!isConnected && isInSet) { connectedDevicesSet.remove(changedAddress); emit q_ptr->deviceDisconnected(changedAddress); } } } void QBluetoothLocalDevicePrivate::InterfacesAdded(const QDBusObjectPath &object_path, InterfaceList interfaces_and_properties) { if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1")) && !deviceChangeMonitors.contains(object_path.path())) { // a new device was added which we need to add to list of known devices if (objectPathIsForThisDevice(deviceAdapterPath, object_path.path())) { OrgFreedesktopDBusPropertiesInterface *monitor = new OrgFreedesktopDBusPropertiesInterface( QStringLiteral("org.bluez"), object_path.path(), QDBusConnection::systemBus()); connect(monitor, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &QBluetoothLocalDevicePrivate::PropertiesChanged); deviceChangeMonitors.insert(object_path.path(), monitor); const QVariantMap ifaceValues = interfaces_and_properties.value(QStringLiteral("org.bluez.Device1")); if (ifaceValues.value(QStringLiteral("Connected"), false).toBool()) { QBluetoothAddress address(ifaceValues.value(QStringLiteral("Address")).toString()); connectedDevicesSet.insert(address); emit q_ptr->deviceConnected(address); } } } if (pairingDiscoveryTimer && pairingDiscoveryTimer->isActive() && interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) { //device discovery for pairing found new remote device OrgBluezDevice1Interface device(QStringLiteral("org.bluez"), object_path.path(), QDBusConnection::systemBus()); if (!address.isNull() && address == QBluetoothAddress(device.address())) processPairing(object_path.path(), pairing); } } void QBluetoothLocalDevicePrivate::InterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces) { if (deviceChangeMonitors.contains(object_path.path()) && interfaces.contains(QLatin1String("org.bluez.Device1"))) { if (objectPathIsForThisDevice(deviceAdapterPath, object_path.path())) { //a device was removed delete deviceChangeMonitors.take(object_path.path()); //the path contains the address (e.g.: /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX) //-> use it to update current list of connected devices QString addressString = object_path.path().right(17); addressString.replace(QStringLiteral("_"), QStringLiteral(":")); const QBluetoothAddress address(addressString); bool found = connectedDevicesSet.remove(address); if (found) emit q_ptr->deviceDisconnected(address); } } if (adapter && object_path.path() == adapter->path() && interfaces.contains(QLatin1String("org.bluez.Adapter1"))) { qCDebug(QT_BT_BLUEZ) << "Adapter" << adapter->path() << "was removed"; // current adapter was removed -> invalidate the instance delete adapter; adapter = nullptr; manager->deleteLater(); manager = nullptr; delete adapterProperties; adapterProperties = nullptr; delete pairingTarget; pairingTarget = nullptr; // turn off connectivity monitoring qDeleteAll(deviceChangeMonitors); deviceChangeMonitors.clear(); connectedDevicesSet.clear(); } } bool QBluetoothLocalDevicePrivate::isValid() const { return adapter && manager; } QList QBluetoothLocalDevicePrivate::connectedDevices() const { return connectedDevicesSet.values(); } void QBluetoothLocalDevicePrivate::pairingCompleted(QDBusPendingCallWatcher *watcher) { Q_Q(QBluetoothLocalDevice); QDBusPendingReply<> reply = *watcher; if (reply.isError()) { qCWarning(QT_BT_BLUEZ) << "Failed to create pairing" << reply.error().name(); const bool canceledByUs = (reply.error().name() == QStringLiteral("org.bluez.Error.AuthenticationCanceled")) && pairingRequestCanceled; if (!canceledByUs) emit q->errorOccurred(QBluetoothLocalDevice::PairingError); pairingRequestCanceled = false; watcher->deleteLater(); return; } pairingRequestCanceled = false; if (adapter) { if (!pairingTarget) { qCWarning(QT_BT_BLUEZ) << "Pairing target expected but found null pointer."; emit q->errorOccurred(QBluetoothLocalDevice::PairingError); watcher->deleteLater(); return; } if (!pairingTarget->paired()) { qCWarning(QT_BT_BLUEZ) << "Device was not paired as requested"; emit q->errorOccurred(QBluetoothLocalDevice::PairingError); watcher->deleteLater(); return; } const QBluetoothAddress targetAddress(pairingTarget->address()); if (pairing == QBluetoothLocalDevice::AuthorizedPaired && !pairingTarget->trusted()) pairingTarget->setTrusted(true); else if (pairing == QBluetoothLocalDevice::Paired && pairingTarget->trusted()) pairingTarget->setTrusted(false); delete pairingTarget; pairingTarget = nullptr; emit q->pairingFinished(targetAddress, pairing); } watcher->deleteLater(); } QT_END_NAMESPACE #include "moc_qbluetoothlocaldevice_p.cpp"