blob: d9883d28cf7fb4f1f80831ff1213613e590f343a [file] [log] [blame]
/****************************************************************************
**
** 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