summaryrefslogtreecommitdiffstats
path: root/src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm')
-rw-r--r--src/bluetooth/qbluetoothdevicediscoveryagent_darwin.mm552
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