diff options
Diffstat (limited to 'src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm')
-rw-r--r-- | src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm b/src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm new file mode 100644 index 00000000..d9883d28 --- /dev/null +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm @@ -0,0 +1,552 @@ +/**************************************************************************** +** +** 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 "qbluetoothdevicediscoveryagent_p.h" +#include "qbluetoothdevicediscoveryagent.h" + +#include "osx/osxbtledeviceinquiry_p.h" +#ifdef Q_OS_MACOS +#include "osx/osxbtdeviceinquiry_p.h" +#include "osx/osxbtsdpinquiry_p.h" +#endif // Q_OS_MACOS +#include "qbluetoothdeviceinfo.h" +#include "osx/osxbtnotifier_p.h" +#include "osx/osxbtutility_p.h" +#include "osx/osxbluetooth_p.h" +#include "osx/uistrings_p.h" +#include "qbluetoothhostinfo.h" +#include "qbluetoothaddress.h" +#include "osx/uistrings_p.h" +#include "qbluetoothuuid.h" +#include "osx/btraii_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qscopedpointer.h> +#include <QtCore/qvector.h> +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> +#include <QtCore/qdebug.h> + +#include <Foundation/Foundation.h> + +QT_BEGIN_NAMESPACE + +namespace +{ + +void registerQDeviceDiscoveryMetaType() +{ + static bool initDone = false; + if (!initDone) { + qRegisterMetaType<QBluetoothDeviceInfo>(); + qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>(); + initDone = true; + } +} +#ifdef Q_OS_MACOS +using InquiryObjC = QT_MANGLE_NAMESPACE(OSXBTDeviceInquiry); +#endif // Q_OS_MACOS + +using LEInquiryObjC = QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry); + +} //namespace + +QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter, + QBluetoothDeviceDiscoveryAgent *q) : + inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), + lastError(QBluetoothDeviceDiscoveryAgent::NoError), + agentState(NonActive), + adapterAddress(adapter), + startPending(false), + stopPending(false), + lowEnergySearchTimeout(OSXBluetooth::defaultLEScanTimeoutMS), +#ifdef Q_OS_MACOS + requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod), +#else + requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod), +#endif // Q_OS_MACOS + q_ptr(q) +{ + registerQDeviceDiscoveryMetaType(); + + Q_ASSERT_X(q != nullptr, Q_FUNC_INFO, "invalid q_ptr (null)"); + +#ifdef Q_OS_MACOS + IOBluetoothHostController *hostController = [IOBluetoothHostController defaultController]; + if (!hostController || [hostController powerState] != kBluetoothHCIPowerStateON) { + qCCritical(QT_BT_OSX) << "no default host controller or adapter is off"; + return; + } + controller.reset(hostController, DarwinBluetooth::RetainPolicy::doInitialRetain); +#endif +} + +QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() +{ + if (inquiryLE && agentState != NonActive) { + // We want the LE scan to stop as soon as possible. + if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { + // Local variable to be retained ... + LEInquiryObjC *inq = inquiryLE.getAs<LEInquiryObjC>(); + dispatch_sync(leQueue, ^{ + [inq stop]; + }); + } + } +} + +bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const +{ + if (startPending) + return true; + + if (stopPending) + return false; + + return agentState != NonActive; +} + +void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) +{ + Q_ASSERT(!isActive()); + Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); + Q_ASSERT(methods & (QBluetoothDeviceDiscoveryAgent::ClassicMethod + | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)); + +#ifdef Q_OS_MACOS + if (!controller) { + setError(QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); + emit q_ptr->error(lastError); + return; + } +#endif // Q_OS_MACOS + + requestedMethods = methods; + + if (stopPending) { + startPending = true; + return; + } + + // This function (re)starts the scan(s) from the scratch; + // starting from Classic if it's in 'methods' (or LE scan if not). + + agentState = NonActive; + discoveredDevices.clear(); + setError(QBluetoothDeviceDiscoveryAgent::NoError); +#ifdef Q_OS_MACOS + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) + return startClassic(); +#endif // Q_OS_MACOS + + startLE(); +} + +#ifdef Q_OS_MACOS + +void QBluetoothDeviceDiscoveryAgentPrivate::startClassic() +{ + Q_ASSERT(!isActive()); + Q_ASSERT(lastError == QBluetoothDeviceDiscoveryAgent::NoError); + Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod); + Q_ASSERT(agentState == NonActive); + + OSXBluetooth::qt_test_iobluetooth_runloop(); + + if (!inquiry) { + // The first Classic scan for this DDA. + inquiry.reset([[InquiryObjC alloc] initWithDelegate:this], + DarwinBluetooth::RetainPolicy::noInitialRetain); + + if (!inquiry) { + qCCritical(QT_BT_OSX) << "failed to initialize an Classic device inquiry"; + setError(QBluetoothDeviceDiscoveryAgent::UnknownError, + QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); + emit q_ptr->error(lastError); + return; + } + } + + agentState = ClassicScan; + + const IOReturn res = [inquiry.getAs<InquiryObjC>() start]; + if (res != kIOReturnSuccess) { + setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); + agentState = NonActive; + emit q_ptr->error(lastError); + } +} + +#endif // Q_OS_MACOS + +void QBluetoothDeviceDiscoveryAgentPrivate::startLE() +{ + Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); + Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + + using namespace OSXBluetooth; + + QScopedPointer<LECBManagerNotifier> notifier(new LECBManagerNotifier); + // Connections: + using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error); + notifier->connect(notifier.data(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError), + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError); + notifier->connect(notifier.data(), &LECBManagerNotifier::LEnotSupported, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported); + notifier->connect(notifier.data(), &LECBManagerNotifier::discoveryFinished, + this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished); + using DeviceMemFunPtr = void (QBluetoothDeviceDiscoveryAgentPrivate::*)(const QBluetoothDeviceInfo &); + notifier->connect(notifier.data(), &LECBManagerNotifier::deviceDiscovered, + this, DeviceMemFunPtr(&QBluetoothDeviceDiscoveryAgentPrivate::deviceFound)); + + // Check queue and create scanner: + inquiryLE.reset([[LEInquiryObjC alloc] initWithNotifier:notifier.data()], + DarwinBluetooth::RetainPolicy::noInitialRetain); + if (inquiryLE) + notifier.take(); // Whatever happens next, inquiryLE is already the owner ... + + dispatch_queue_t leQueue(qt_LE_queue()); + if (!leQueue || !inquiryLE) { + setError(QBluetoothDeviceDiscoveryAgent::UnknownError, + QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED_LE)); + agentState = NonActive; + emit q_ptr->error(lastError); + return; + } + + // Now start in on LE queue: + agentState = LEScan; + // We need the local variable so that it's retained ... + LEInquiryObjC *inq = inquiryLE.getAs<LEInquiryObjC>(); + dispatch_async(leQueue, ^{ + [inq startWithTimeout:lowEnergySearchTimeout]; + }); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::stop() +{ + Q_ASSERT_X(isActive(), Q_FUNC_INFO, "called whithout active inquiry"); + Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, + Q_FUNC_INFO, "called with invalid bluetooth adapter"); + + using namespace OSXBluetooth; + + const bool prevStart = startPending; + startPending = false; + stopPending = true; + + setError(QBluetoothDeviceDiscoveryAgent::NoError); + +#ifdef Q_OS_MACOS + if (agentState == ClassicScan) { + const IOReturn res = [inquiry.getAs<InquiryObjC>() stop]; + if (res != kIOReturnSuccess) { + qCWarning(QT_BT_OSX) << "failed to stop"; + startPending = prevStart; + stopPending = false; + setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STOPPED)); + emit q_ptr->error(lastError); + } + } else { +#else + { + Q_UNUSED(prevStart) +#endif // Q_OS_MACOS + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + // We need the local variable so that it's retained ... + LEInquiryObjC *inq = inquiryLE.getAs<LEInquiryObjC>(); + dispatch_sync(leQueue, ^{ + [inq stop]; + }); + // We consider LE scan to be stopped immediately and + // do not care about this LEDeviceInquiry object anymore. + LEinquiryFinished(); + } +} + +#ifdef Q_OS_MACOS + +void QBluetoothDeviceDiscoveryAgentPrivate::inquiryFinished() +{ + // The subsequent start(LE) function (if any) + // will (re)set the correct state. + agentState = NonActive; + + if (stopPending && !startPending) { + stopPending = false; + emit q_ptr->canceled(); + } else if (startPending) { + startPending = false; + stopPending = false; + start(requestedMethods); + } else { + // We can be here _only_ if a classic scan + // finished in a normal way (not cancelled) + // and requestedMethods includes LowEnergyMethod. + // startLE() will take care of old devices + // not supporting Bluetooth 4.0. + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) + startLE(); + else + emit q_ptr->finished(); + } +} + +void QBluetoothDeviceDiscoveryAgentPrivate::error(IOReturn error) +{ + startPending = false; + stopPending = false; + + setError(error); + + emit q_ptr->error(lastError); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::classicDeviceFound(void *obj) +{ + auto device = static_cast<IOBluetoothDevice *>(obj); + Q_ASSERT_X(device, Q_FUNC_INFO, "invalid IOBluetoothDevice (nil)"); + + Q_ASSERT_X(agentState == ClassicScan, Q_FUNC_INFO, + "invalid agent state (expected classic scan)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + // Let's collect some info about this device: + const QBluetoothAddress deviceAddress(OSXBluetooth::qt_address([device getAddress])); + if (deviceAddress.isNull()) { + qCWarning(QT_BT_OSX) << "invalid Bluetooth address"; + return; + } + + QString deviceName; + if (device.name) + deviceName = QString::fromNSString(device.name); + + const auto classOfDevice = qint32(device.classOfDevice); + + QBluetoothDeviceInfo deviceInfo(deviceAddress, deviceName, classOfDevice); + deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration); + deviceInfo.setRssi(device.RSSI); + + const QVector<QBluetoothUuid> uuids(OSXBluetooth::extract_services_uuids(device)); + deviceInfo.setServiceUuids(uuids); + + deviceFound(deviceInfo); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::setError(IOReturn error, const QString &text) +{ + if (error == kIOReturnSuccess) + setError(QBluetoothDeviceDiscoveryAgent::NoError, text); + else if (error == kIOReturnNoPower) + setError(QBluetoothDeviceDiscoveryAgent::PoweredOffError, text); + else + setError(QBluetoothDeviceDiscoveryAgent::UnknownError, text); +} + +#endif // Q_OS_MACOS + +void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text) +{ + lastError = error; + + if (text.length() > 0) { + errorString = text; + } else { + switch (lastError) { + case QBluetoothDeviceDiscoveryAgent::NoError: + errorString = QString(); + break; + case QBluetoothDeviceDiscoveryAgent::PoweredOffError: + errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_POWERED_OFF); + break; + case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError: + errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_INVALID_ADAPTER); + break; + case QBluetoothDeviceDiscoveryAgent::InputOutputError: + errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_IO); + break; + case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError: + errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_NOTSUPPORTED); + break; + case QBluetoothDeviceDiscoveryAgent::UnknownError: + default: + errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_UNKNOWN_ERROR); + } + } +} + +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error) +{ + Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError + || error == QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + + inquiryLE.reset(); + + startPending = false; + stopPending = false; + agentState = NonActive; + setError(error); + emit q_ptr->error(lastError); +} + +void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() +{ + qCDebug(QT_BT_OSX) << "no Bluetooth LE support"; + +#ifdef Q_OS_MACOS + if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) { + // Having both Classic | LE means this is not an error. + LEinquiryFinished(); + } else { + // In the past this was never an error, that's why we have + // LEnotSupported as a special method. But now, since + // we can have separate Classic/LE scans, we have to report it + // as UnsupportedDiscoveryMethod. + LEinquiryError(QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); + } +#else + inquiryLE.reset(); + startPending = false; + stopPending = false; + setError(QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError); + emit q_ptr->error(lastError); +#endif +} + +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() +{ + // The same logic as in inquiryFinished, but does not start LE scan. + agentState = NonActive; + inquiryLE.reset(); + + if (stopPending && !startPending) { + stopPending = false; + emit q_ptr->canceled(); + } else if (startPending) { + startPending = false; + stopPending = false; + start(requestedMethods); //Start again. + } else { + emit q_ptr->finished(); + } +} + +void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QBluetoothDeviceInfo &newDeviceInfo) +{ + // Core Bluetooth does not allow us to access addresses, we have to use uuid instead. + // This uuid has nothing to do with uuids in Bluetooth in general (it's generated by + // Apple's framework using some algorithm), but it's a 128-bit uuid after all. + const bool isLE = +#ifdef Q_OS_MACOS + newDeviceInfo.coreConfigurations() == QBluetoothDeviceInfo::LowEnergyCoreConfiguration; +#else + true; +#endif // Q_OS_MACOS + for (int i = 0, e = discoveredDevices.size(); i < e; ++i) { + if (isLE) { + if (discoveredDevices[i].deviceUuid() == newDeviceInfo.deviceUuid()) { + QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None; + if (discoveredDevices[i].rssi() != newDeviceInfo.rssi()) { + qCDebug(QT_BT_OSX) << "Updating RSSI for" << newDeviceInfo.address() + << newDeviceInfo.rssi(); + discoveredDevices[i].setRssi(newDeviceInfo.rssi()); + updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI); + } + + if (discoveredDevices[i].manufacturerData() != newDeviceInfo.manufacturerData()) { + qCDebug(QT_BT_OSX) << "Updating manufacturer data for" << newDeviceInfo.address(); + const QVector<quint16> keys = newDeviceInfo.manufacturerIds(); + for (auto key: keys) + discoveredDevices[i].setManufacturerData(key, newDeviceInfo.manufacturerData(key)); + updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData); + } + + if (lowEnergySearchTimeout > 0) { + if (discoveredDevices[i] != newDeviceInfo) { + discoveredDevices.replace(i, newDeviceInfo); + emit q_ptr->deviceDiscovered(newDeviceInfo); + } else { + if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) + emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields); + } + + return; + } + + discoveredDevices.replace(i, newDeviceInfo); + emit q_ptr->deviceDiscovered(newDeviceInfo); + + if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) + emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields); + + return; + } + } else { +#ifdef Q_OS_MACOS + if (discoveredDevices[i].address() == newDeviceInfo.address()) { + if (discoveredDevices[i] == newDeviceInfo) + return; + + discoveredDevices.replace(i, newDeviceInfo); + emit q_ptr->deviceDiscovered(newDeviceInfo); + return; + } +#else + Q_UNREACHABLE(); +#endif // Q_OS_MACOS + } + } + + discoveredDevices.append(newDeviceInfo); + emit q_ptr->deviceDiscovered(newDeviceInfo); +} + +QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() +{ +#ifdef Q_OS_MACOS + return ClassicMethod | LowEnergyMethod; +#else + return LowEnergyMethod; +#endif // Q_OS_MACOS +} + +QT_END_NAMESPACE |