Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QtTest>
#include <QDebug>
#include <QVariant>
#include <QList>
#include <QLoggingCategory>
#include "../../shared/bttestutil_p.h"
#include <private/qtbluetoothglobal_p.h>
#include <qbluetoothaddress.h>
#include <qbluetoothdevicediscoveryagent.h>
#include <qbluetoothlocaldevice.h>
#if QT_CONFIG(permissions)
#include <QtCore/qcoreapplication.h>
#include <QtCore/qpermissions.h>
#include <QtCore/qnamespace.h>
#endif // permissions
#ifdef Q_OS_ANDROID
#include <QtCore/qjnienvironment.h>
#endif
#ifdef Q_OS_WIN
#include <objbase.h> // for CoInitializeEx() and friends
#endif
#include <memory>
using namespace Qt::StringLiterals;
QT_USE_NAMESPACE
/*
* Some parts of this test require a remote and discoverable Bluetooth
* device. Setting the BT_TEST_DEVICE environment variable will
* set the test up to fail if it cannot find a remote device.
* BT_TEST_DEVICE should contain the address of the device the
* test expects to find. Ensure that the device is running
* in discovery mode.
**/
// Maximum time to for bluetooth device scan
const int MaxScanTime = 5 * 60 * 1000; // 5 minutes in ms
//Bluez needs at least 10s for a device discovery to be cancelled
const int MaxWaitForCancelTime = 15 * 1000; // 15 seconds in ms
#ifdef Q_OS_ANDROID
// Android is sometimes unable to cancel immediately
const int WaitBeforeStopTime = 200;
extern void testEnumToString(std::function<void(const char*, int)> callback);
extern void parseScanRecord(QBluetoothDeviceInfo &, jbyteArray);
#endif
class tst_QBluetoothDeviceDiscoveryAgent : public QObject
{
Q_OBJECT
public:
tst_QBluetoothDeviceDiscoveryAgent();
~tst_QBluetoothDeviceDiscoveryAgent();
public slots:
void deviceDiscoveryDebug(const QBluetoothDeviceInfo &info);
void finished();
private slots:
void initTestCase();
void tst_invalidBtAddress();
void tst_startStopDeviceDiscoveries();
void tst_deviceDiscovery();
void tst_discoveryTimeout();
void tst_discoveryMethods();
void tst_majorMinorMapping();
void tst_androidScanRecordParsing_data();
void tst_androidScanRecordParsing();
void tst_windowsDiscoveryInThread_data();
void tst_windowsDiscoveryInThread();
private:
qsizetype noOfLocalDevices;
using DiscoveryAgentPtr = std::unique_ptr<QBluetoothDeviceDiscoveryAgent>;
#if QT_CONFIG(permissions)
Qt::PermissionStatus permissionStatus = Qt::PermissionStatus::Undetermined;
#endif
};
tst_QBluetoothDeviceDiscoveryAgent::tst_QBluetoothDeviceDiscoveryAgent()
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true"));
qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>();
#if QT_CONFIG(permissions)
permissionStatus = qApp->checkPermission(QBluetoothPermission{});
const bool ciRun = qEnvironmentVariable("QTEST_ENVIRONMENT").split(' ').contains("ci");
if (!ciRun && permissionStatus == Qt::PermissionStatus::Undetermined) {
QTestEventLoop loop;
qApp->requestPermission(QBluetoothPermission{},
&loop, [this, &loop](const QPermission &permission) {
permissionStatus = permission.status();
loop.exitLoop();
});
if (permissionStatus == Qt::PermissionStatus::Undetermined)
loop.enterLoopMSecs(30000);
}
#endif // QT_CONFIG(permissions)
}
tst_QBluetoothDeviceDiscoveryAgent::~tst_QBluetoothDeviceDiscoveryAgent()
{
}
void tst_QBluetoothDeviceDiscoveryAgent::initTestCase()
{
qRegisterMetaType<QBluetoothDeviceInfo>();
noOfLocalDevices = QBluetoothLocalDevice::allDevices().size();
if (!noOfLocalDevices)
return;
// turn on BT in case it is not on
QBluetoothLocalDevice *device = new QBluetoothLocalDevice();
if (device->hostMode() == QBluetoothLocalDevice::HostPoweredOff) {
QSignalSpy hostModeSpy(device, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)));
QVERIFY(hostModeSpy.isEmpty());
device->powerOn();
int connectTime = 5000; // ms
while (hostModeSpy.isEmpty() && connectTime > 0) {
QTest::qWait(500);
connectTime -= 500;
}
QVERIFY(!hostModeSpy.isEmpty());
}
QBluetoothLocalDevice::HostMode hostMode= device->hostMode();
QVERIFY(hostMode == QBluetoothLocalDevice::HostConnectable
|| hostMode == QBluetoothLocalDevice::HostDiscoverable
|| hostMode == QBluetoothLocalDevice::HostDiscoverableLimitedInquiry);
delete device;
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_invalidBtAddress()
{
DiscoveryAgentPtr discoveryAgent(new QBluetoothDeviceDiscoveryAgent(
QBluetoothAddress(QStringLiteral("11:11:11:11:11:11"))));
QCOMPARE(discoveryAgent->error(), QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError);
discoveryAgent->start();
QCOMPARE(discoveryAgent->isActive(), false);
#if QT_CONFIG(permissions)
if (permissionStatus != Qt::PermissionStatus::Granted)
return;
#endif
discoveryAgent.reset(new QBluetoothDeviceDiscoveryAgent(QBluetoothAddress()));
QCOMPARE(discoveryAgent->error(), QBluetoothDeviceDiscoveryAgent::NoError);
if (!QBluetoothLocalDevice::allDevices().isEmpty()) {
discoveryAgent->start();
QCOMPARE(discoveryAgent->isActive(), true);
}
}
void tst_QBluetoothDeviceDiscoveryAgent::deviceDiscoveryDebug(const QBluetoothDeviceInfo &info)
{
qDebug() << "Discovered device:" << info.address().toString() << info.name();
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_startStopDeviceDiscoveries()
{
QBluetoothDeviceDiscoveryAgent discoveryAgent;
QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
QVERIFY(discoveryAgent.errorString().isEmpty());
QVERIFY(!discoveryAgent.isActive());
QVERIFY(discoveryAgent.discoveredDevices().isEmpty());
QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished()));
QSignalSpy cancelSpy(&discoveryAgent, SIGNAL(canceled()));
QSignalSpy errorSpy(&discoveryAgent,
SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));
// Starting case 1: start-stop, expecting cancel signal
// we should have no errors at this point.
QVERIFY(errorSpy.isEmpty());
discoveryAgent.start();
#if QT_CONFIG(permissions)
if (permissionStatus != Qt::PermissionStatus::Granted) {
// If bluetooth is OFF, the permission does not get checked (e.g. on Darwin),
// but not to depend on the order in which errors generated, we
// do not compare error value with MissionPermissionsError here.
return;
}
#endif
if (errorSpy.isEmpty()) {
QVERIFY(discoveryAgent.isActive());
QCOMPARE(discoveryAgent.errorString(), QString());
QCOMPARE(discoveryAgent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
} else {
QCOMPARE(noOfLocalDevices, 0);
QVERIFY(!discoveryAgent.isActive());
QVERIFY(!discoveryAgent.errorString().isEmpty());
QVERIFY(discoveryAgent.error() != QBluetoothDeviceDiscoveryAgent::NoError);
QSKIP("No local Bluetooth device available. Skipping remaining part of test.");
}
// cancel current request.
#ifdef Q_OS_ANDROID
// Android sometimes can't cancel immediately (~on the same millisecond),
// but instead a "pending cancel" happens, which means the discovery will be
// canceled at a later point in time. When this happens the Android backend
// also emits an immediate errorOccurred(). While this seems to be as intended,
// as a result many parts of this test function may fail (not always).
//
// In this test function we wait some milliseconds between start() and stop() to
// bypass this behavior difference. This is to avoid complex iffery (Android itself
// can behave differently every time) and ifdeffery (Q_OS_ANDROID) in the test
QTest::qWait(WaitBeforeStopTime);
#endif
discoveryAgent.stop();
// Wait for up to MaxWaitForCancelTime for the cancel to finish
QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);
// we should not be active anymore
QVERIFY(!discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
QCOMPARE(cancelSpy.size(), 1);
cancelSpy.clear();
// Starting case 2: start-start-stop, expecting cancel signal
discoveryAgent.start();
// we should be active now
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// start again. should this be error?
discoveryAgent.start();
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// stop
#ifdef Q_OS_ANDROID
QTest::qWait(WaitBeforeStopTime);
#endif
discoveryAgent.stop();
// Wait for up to MaxWaitForCancelTime for the cancel to finish
QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);
// we should not be active anymore
QVERIFY(!discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
QCOMPARE(cancelSpy.size(), 1);
cancelSpy.clear();
// Starting case 3: stop
discoveryAgent.stop();
QVERIFY(!discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// Don't expect finished signal and no error
QVERIFY(finishedSpy.isEmpty());
QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
QVERIFY(discoveryAgent.errorString().isEmpty());
/*
Starting case 4: start-stop-start-stop:
We are testing that two subsequent stop() calls reduce total number
of cancel() signals to 1 if the true cancellation requires
asynchronous function calls (signal consolidation); otherwise we
expect 2x cancel() signal.
Examples are:
- Bluez4 (event loop needs to run for cancel)
- Bluez5 (no event loop required)
*/
bool immediateSignal = false;
discoveryAgent.start();
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// cancel current request.
#ifdef Q_OS_ANDROID
QTest::qWait(WaitBeforeStopTime);
#endif
discoveryAgent.stop();
//should only have triggered cancel() if stop didn't involve the event loop
if (cancelSpy.size() == 1) immediateSignal = true;
// start a new one
discoveryAgent.start();
// we should be active now
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// stop
#ifdef Q_OS_ANDROID
QTest::qWait(WaitBeforeStopTime);
#endif
discoveryAgent.stop();
if (immediateSignal)
QCOMPARE(cancelSpy.size(), 2);
// Wait for up to MaxWaitForCancelTime for the cancel to finish
QTRY_VERIFY_WITH_TIMEOUT(!cancelSpy.isEmpty(), MaxWaitForCancelTime);
// we should not be active anymore
QVERIFY(!discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// should only have 1 cancel
if (immediateSignal)
QCOMPARE(cancelSpy.size(), 2);
else
QCOMPARE(cancelSpy.size(), 1);
cancelSpy.clear();
// Starting case 5: start-stop-start: expecting finished signal & no cancel
discoveryAgent.start();
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// cancel current request.
#ifdef Q_OS_ANDROID
QTest::qWait(WaitBeforeStopTime);
#endif
discoveryAgent.stop();
// start a new one
discoveryAgent.start();
// we should be active now
QVERIFY(discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// Wait for up to MaxScanTime for the scan to finish
QTRY_VERIFY_WITH_TIMEOUT(!finishedSpy.isEmpty(), MaxScanTime);
// we should not be active anymore
QVERIFY(!discoveryAgent.isActive());
QVERIFY(errorSpy.isEmpty());
// should only have 1 cancel
QCOMPARE(finishedSpy.size(), 1);
// On OS X, stop is synchronous (signal will be emitted immediately).
if (!immediateSignal)
QVERIFY(cancelSpy.isEmpty());
}
void tst_QBluetoothDeviceDiscoveryAgent::finished()
{
qDebug() << "Finished called";
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_deviceDiscovery()
{
{
//Run test in case of multiple Bluetooth adapters
QBluetoothLocalDevice localDevice;
//We will use default adapter if there is no other adapter
QBluetoothAddress address = localDevice.address();
QBluetoothDeviceDiscoveryAgent discoveryAgent(address);
QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
QVERIFY(discoveryAgent.errorString().isEmpty());
QVERIFY(!discoveryAgent.isActive());
QVERIFY(discoveryAgent.discoveredDevices().isEmpty());
QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished()));
QSignalSpy errorSpy(&discoveryAgent,
SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));
QSignalSpy discoveredSpy(&discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)));
// connect(&discoveryAgent, SIGNAL(finished()), this, SLOT(finished()));
// connect(&discoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),
// this, SLOT(deviceDiscoveryDebug(QBluetoothDeviceInfo)));
discoveryAgent.start();
if (!errorSpy.isEmpty()) {
#if QT_CONFIG(permissions)
if (permissionStatus == Qt::PermissionStatus::Granted)
#endif
QCOMPARE(noOfLocalDevices, 0);
QVERIFY(!discoveryAgent.isActive());
QSKIP("No local Bluetooth device available. Skipping remaining part of test.");
}
QVERIFY(discoveryAgent.isActive());
// Wait for up to MaxScanTime for the scan to finish
int scanTime = MaxScanTime;
while (finishedSpy.isEmpty() && scanTime > 0) {
QTest::qWait(15000);
scanTime -= 15000;
}
// verify that we are finished
QVERIFY(!discoveryAgent.isActive());
// stop
discoveryAgent.stop();
QVERIFY(!discoveryAgent.isActive());
qDebug() << "Scan time left:" << scanTime;
// Expect finished signal with no error
QVERIFY(finishedSpy.size() == 1);
QVERIFY(errorSpy.isEmpty());
QVERIFY(discoveryAgent.error() == discoveryAgent.NoError);
QVERIFY(discoveryAgent.errorString().isEmpty());
// verify that the list is as big as the signals received.
// discoveredSpy might have more events as some devices are found multiple times,
// leading to messages like
// "Almost Duplicate "88:C6:26:F5:3E:E2" "88-C6-26-F5-3E-E2" - replacing in place"
QVERIFY(discoveredSpy.size() >= discoveryAgent.discoveredDevices().size());
// verify that there really was some devices in the array
const QString remote = qEnvironmentVariable("BT_TEST_DEVICE");
QBluetoothAddress remoteDevice;
if (!remote.isEmpty()) {
remoteDevice = QBluetoothAddress(remote);
QVERIFY2(!remote.isNull(), "Expecting valid Bluetooth address to be passed via BT_TEST_DEVICE");
} else {
qWarning() << "Not using a remote device for testing. Set BT_TEST_DEVICE env to run extended tests involving a remote device";
}
if (!remoteDevice.isNull())
QVERIFY(!discoveredSpy.isEmpty());
// All returned QBluetoothDeviceInfo should be valid.
while (!discoveredSpy.isEmpty()) {
const QBluetoothDeviceInfo info =
qvariant_cast<QBluetoothDeviceInfo>(discoveredSpy.takeFirst().at(0));
QVERIFY(info.isValid());
}
}
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryTimeout()
{
QBluetoothDeviceDiscoveryAgent agent;
// check default values
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) || defined(Q_OS_ANDROID) || QT_CONFIG(winrt_bt) \
|| QT_CONFIG(bluez)
QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 40000);
agent.setLowEnergyDiscoveryTimeout(-1); // negative ignored
QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 40000);
agent.setLowEnergyDiscoveryTimeout(20000);
QCOMPARE(agent.lowEnergyDiscoveryTimeout(), 20000);
#else
QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1);
agent.setLowEnergyDiscoveryTimeout(20000); // feature not supported -> ignored
QCOMPARE(agent.lowEnergyDiscoveryTimeout(), -1);
#endif
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryMethods()
{
const QBluetoothLocalDevice localDevice;
if (localDevice.allDevices().size() != 1) {
// On iOS it returns 0 but we still have working BT.
#ifndef Q_OS_IOS
QSKIP("This test expects exactly one local device working");
#endif
}
const QBluetoothDeviceDiscoveryAgent::DiscoveryMethods
supportedMethods = QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods();
QVERIFY(supportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod);
QBluetoothDeviceDiscoveryAgent::DiscoveryMethod
unsupportedMethods = QBluetoothDeviceDiscoveryAgent::NoMethod;
QBluetoothDeviceInfo::CoreConfiguration
expectedConfiguration = QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration;
if (supportedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
unsupportedMethods = QBluetoothDeviceDiscoveryAgent::LowEnergyMethod;
expectedConfiguration = QBluetoothDeviceInfo::BaseRateCoreConfiguration;
} else if (supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) {
unsupportedMethods = QBluetoothDeviceDiscoveryAgent::ClassicMethod;
expectedConfiguration = QBluetoothDeviceInfo::LowEnergyCoreConfiguration;
}
QBluetoothDeviceDiscoveryAgent agent;
QSignalSpy finishedSpy(&agent, SIGNAL(finished()));
QSignalSpy errorSpy(&agent, SIGNAL(errorOccurred(QBluetoothDeviceDiscoveryAgent::Error)));
QSignalSpy discoveredSpy(&agent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)));
// NoMethod - should just immediately return:
agent.start(QBluetoothDeviceDiscoveryAgent::NoMethod);
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
QVERIFY(!agent.isActive());
QCOMPARE(finishedSpy.size(), 0);
QCOMPARE(errorSpy.size(), 0);
QCOMPARE(discoveredSpy.size(), 0);
if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod) {
agent.start(unsupportedMethods);
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
QVERIFY(!agent.isActive());
QVERIFY(finishedSpy.isEmpty());
QCOMPARE(errorSpy.size(), 1);
errorSpy.clear();
QVERIFY(discoveredSpy.isEmpty());
}
// Start discovery, probably both Classic and LE methods:
agent.start(supportedMethods);
#if QT_CONFIG(permissions)
if (permissionStatus != Qt::PermissionStatus::Granted) {
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::MissingPermissionsError);
QSKIP("The remaining test requires the Bluetooth permission granted");
}
#endif
QVERIFY(agent.isActive());
QVERIFY(errorSpy.isEmpty());
#define RUN_DISCOVERY(maxTimeout, step, condition) \
for (int scanTime = maxTimeout; (condition) && scanTime > 0; scanTime -= step) \
QTest::qWait(step);
// Wait for up to MaxScanTime for the scan to finish
const int timeStep = 15000;
RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty())
QVERIFY(!agent.isActive());
QVERIFY(errorSpy.size() <= 1);
if (errorSpy.size()) {
// For example, old iOS device could report it supports LE method,
// but it actually does not.
QVERIFY(supportedMethods == QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
} else {
QVERIFY(finishedSpy.size() == 1);
QVERIFY(agent.error() == QBluetoothDeviceDiscoveryAgent::NoError);
QVERIFY(agent.errorString().isEmpty());
while (!discoveredSpy.isEmpty()) {
const QBluetoothDeviceInfo info =
qvariant_cast<QBluetoothDeviceInfo>(discoveredSpy.takeFirst().at(0));
QVERIFY(info.isValid());
// on Android we do find devices with unknown configuration
if (info.coreConfigurations() != QBluetoothDeviceInfo::UnknownCoreConfiguration)
QVERIFY(info.coreConfigurations() & expectedConfiguration);
}
}
if (unsupportedMethods != QBluetoothDeviceDiscoveryAgent::NoMethod)
return;
// Both methods were reported as supported. We already tested them
// above, now let's test first Classic then LE.
finishedSpy.clear();
errorSpy.clear();
discoveredSpy.clear();
agent.start(QBluetoothDeviceDiscoveryAgent::ClassicMethod);
QVERIFY(agent.isActive());
QVERIFY(errorSpy.isEmpty());
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty())
QVERIFY(!agent.isActive());
QVERIFY(errorSpy.isEmpty());
QCOMPARE(finishedSpy.size(), 1);
finishedSpy.clear();
discoveredSpy.clear();
agent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
QVERIFY(agent.isActive());
QVERIFY(errorSpy.isEmpty());
RUN_DISCOVERY(MaxScanTime, timeStep, finishedSpy.isEmpty() && errorSpy.isEmpty())
QVERIFY(!agent.isActive());
QVERIFY(errorSpy.size() <= 1);
if (errorSpy.size()) {
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
qDebug() << "QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods is inaccurate"
" on your platform/with your device, LowEnergyMethod is not supported";
} else {
QCOMPARE(agent.error(), QBluetoothDeviceDiscoveryAgent::NoError);
QCOMPARE(finishedSpy.size(), 1);
}
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_majorMinorMapping()
{
#if !defined(Q_OS_ANDROID)
QSKIP("This test is only pertinent to Android!");
#else
const QMap<QLatin1StringView, int> expectedMapping = {
// AUDIO_VIDEO
{ "AUDIO_VIDEO_CAMCORDER"_L1, QBluetoothDeviceInfo::Camcorder },
{ "AUDIO_VIDEO_CAR_AUDIO"_L1, QBluetoothDeviceInfo::CarAudio },
{ "AUDIO_VIDEO_HANDSFREE"_L1, QBluetoothDeviceInfo::HandsFreeDevice },
{ "AUDIO_VIDEO_HEADPHONES"_L1, QBluetoothDeviceInfo::Headphones },
{ "AUDIO_VIDEO_HIFI_AUDIO"_L1, QBluetoothDeviceInfo::HiFiAudioDevice },
{ "AUDIO_VIDEO_LOUDSPEAKER"_L1, QBluetoothDeviceInfo::Loudspeaker },
{ "AUDIO_VIDEO_MICROPHONE"_L1, QBluetoothDeviceInfo::Microphone },
{ "AUDIO_VIDEO_PORTABLE_AUDIO"_L1, QBluetoothDeviceInfo::PortableAudioDevice },
{ "AUDIO_VIDEO_SET_TOP_BOX"_L1, QBluetoothDeviceInfo::SetTopBox },
{ "AUDIO_VIDEO_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedAudioVideoDevice },
{ "AUDIO_VIDEO_VCR"_L1, QBluetoothDeviceInfo::Vcr },
{ "AUDIO_VIDEO_VIDEO_CAMERA"_L1, QBluetoothDeviceInfo::VideoCamera },
{ "AUDIO_VIDEO_VIDEO_CONFERENCING"_L1, QBluetoothDeviceInfo::VideoConferencing },
{ "AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER"_L1, QBluetoothDeviceInfo::VideoDisplayAndLoudspeaker },
{ "AUDIO_VIDEO_VIDEO_GAMING_TOY"_L1, QBluetoothDeviceInfo::GamingDevice },
{ "AUDIO_VIDEO_VIDEO_MONITOR"_L1, QBluetoothDeviceInfo::VideoMonitor },
{ "AUDIO_VIDEO_WEARABLE_HEADSET"_L1, QBluetoothDeviceInfo::WearableHeadsetDevice },
// COMPUTER
{ "COMPUTER_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedComputer },
{ "COMPUTER_DESKTOP"_L1, QBluetoothDeviceInfo::DesktopComputer },
{ "COMPUTER_HANDHELD_PC_PDA"_L1, QBluetoothDeviceInfo::HandheldComputer },
{ "COMPUTER_LAPTOP"_L1, QBluetoothDeviceInfo::LaptopComputer },
{ "COMPUTER_PALM_SIZE_PC_PDA"_L1, QBluetoothDeviceInfo::HandheldClamShellComputer },
{ "COMPUTER_SERVER"_L1, QBluetoothDeviceInfo::ServerComputer },
{ "COMPUTER_WEARABLE"_L1, QBluetoothDeviceInfo::WearableComputer },
// HEALTH
{ "HEALTH_BLOOD_PRESSURE"_L1, QBluetoothDeviceInfo::HealthBloodPressureMonitor },
{ "HEALTH_DATA_DISPLAY"_L1, QBluetoothDeviceInfo::HealthDataDisplay },
{ "HEALTH_GLUCOSE"_L1, QBluetoothDeviceInfo::HealthGlucoseMeter },
{ "HEALTH_PULSE_OXIMETER"_L1, QBluetoothDeviceInfo::HealthPulseOximeter },
{ "HEALTH_PULSE_RATE"_L1, QBluetoothDeviceInfo::HealthStepCounter },
{ "HEALTH_THERMOMETER"_L1, QBluetoothDeviceInfo::HealthThermometer },
{ "HEALTH_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedHealthDevice },
{ "HEALTH_WEIGHING"_L1, QBluetoothDeviceInfo::HealthWeightScale },
// PHONE
{ "PHONE_CELLULAR"_L1, QBluetoothDeviceInfo::CellularPhone },
{ "PHONE_CORDLESS"_L1, QBluetoothDeviceInfo::CordlessPhone },
{ "PHONE_ISDN"_L1, QBluetoothDeviceInfo::CommonIsdnAccessPhone },
{ "PHONE_MODEM_OR_GATEWAY"_L1, QBluetoothDeviceInfo::WiredModemOrVoiceGatewayPhone },
{ "PHONE_SMART"_L1, QBluetoothDeviceInfo::SmartPhone },
{ "PHONE_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedPhone },
// TOY
{ "TOY_CONTROLLER"_L1, QBluetoothDeviceInfo::ToyController },
{ "TOY_DOLL_ACTION_FIGURE"_L1, QBluetoothDeviceInfo::ToyDoll },
{ "TOY_GAME"_L1, QBluetoothDeviceInfo::ToyGame },
{ "TOY_ROBOT"_L1, QBluetoothDeviceInfo::ToyRobot },
{ "TOY_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedToy },
{ "TOY_VEHICLE"_L1, QBluetoothDeviceInfo::ToyVehicle },
// WEARABLE
{ "WEARABLE_GLASSES"_L1, QBluetoothDeviceInfo::WearableGlasses },
{ "WEARABLE_HELMET"_L1, QBluetoothDeviceInfo::WearableHelmet },
{ "WEARABLE_JACKET"_L1, QBluetoothDeviceInfo::WearableJacket },
{ "WEARABLE_PAGER"_L1, QBluetoothDeviceInfo::WearablePager },
{ "WEARABLE_UNCATEGORIZED"_L1, QBluetoothDeviceInfo::UncategorizedWearableDevice },
{ "WEARABLE_WRIST_WATCH"_L1, QBluetoothDeviceInfo::WearableWristWatch },
// PERIPHERAL
{ "PERIPHERAL_NON_KEYBOARD_NON_POINTING"_L1, QBluetoothDeviceInfo::UncategorizedPeripheral },
{ "PERIPHERAL_KEYBOARD"_L1, QBluetoothDeviceInfo::KeyboardPeripheral },
{ "PERIPHERAL_POINTING"_L1, QBluetoothDeviceInfo::PointingDevicePeripheral },
{ "PERIPHERAL_KEYBOARD_POINTING"_L1, QBluetoothDeviceInfo::KeyboardWithPointingDevicePeripheral }
};
qsizetype hits = 0;
testEnumToString([&](const char *generatedName, int minor) {
QLatin1StringView generatedNameL1(generatedName);
auto printOnFailure = qScopeGuard([&] { qDebug("%s", generatedName); });
QVERIFY(expectedMapping.contains(generatedNameL1));
int expectedValue = expectedMapping.value(generatedNameL1);
QCOMPARE(minor, expectedValue);
printOnFailure.dismiss();
++hits;
});
QCOMPARE(hits, expectedMapping.size());
#endif
}
void tst_QBluetoothDeviceDiscoveryAgent::tst_androidScanRecordParsing_data()
{
#ifndef Q_OS_ANDROID
QSKIP("This test only runs on Android");
#endif
QTest::addColumn<QByteArray>("scanRecord");
QTest::addColumn<int>("expectedServiceUuidCount");
QTest::addColumn<bool>("expectName");
QTest::addColumn<bool>("expectManufacturerData");
// -----------------------------------------------------------------------
// Baseline: well-formed scan record containing a 16-bit service UUID,
// a complete local name, and manufacturer-specific data.
// All three fields must be parsed correctly.
//
// Layout (each field: [nBytes] [ADType] [data...]):
// 02 01 06 -- Flags (ADType 0x01); not parsed by Qt, skipped
// 03 03 34 12 -- 16-bit UUID complete (0x1234 in little-endian)
// 05 09 54 65 73 74 -- Complete local name: "Test"
// 05 FF E0 00 AB CD -- Manufacturer data: company=0x00E0, payload=0xAB 0xCD
// -----------------------------------------------------------------------
QByteArray validRecord = QByteArray::fromHex("020106" // Flags
"03033412" // 16-bit UUID 0x1234
"050954657374"// "Test"
"05FFE000ABCD"); // Mfr data
QTest::newRow("valid_record") << validRecord << 1 << true << true;
QTest::newRow("zero_length") << QByteArray(1, 0x00) << 0 << false << false;
QByteArray truncated;
truncated += char(0x0A); // nBytes = 10, claims 9 bytes of name data
truncated += char(0x09); // ADType: complete local name
truncated += QByteArray("Hi"); // only 2 bytes available — record is truncated
QTest::newRow("truncated_record") << truncated << 0 << false << false;
// -----------------------------------------------------------------------
// Issue 1 – length byte with the high bit set (e.g. 0xFF).
//
// When the length field is read as a signed char (the bug), 0xFF becomes
// -1, making 'i += nBytes + 1' advance by zero — causing an infinite loop.
//
// This test will hang indefinitely with the unfixed implementation.
// -----------------------------------------------------------------------
QByteArray highBitLength;
highBitLength += char(0xFF); // length byte — high bit set
highBitLength += char(0x09); // ADType: complete local name
highBitLength += QByteArray(30, 'A'); // 30 bytes of name data
QTest::newRow("length_high_bit_set") << highBitLength << 0 << false << false;
// -----------------------------------------------------------------------
// Issue 2a – 128-bit UUID AD type with only 1 data byte (nBytes = 2).
//
// ADType128BitUuidComplete requires 16 bytes of data, so we'll read past
// the end of the buffer without a proper length check.
//
// After the fix (per-case guard: if (nBytes >= 17)), no read occurs and
// no UUID is produced.
// -----------------------------------------------------------------------
QByteArray short128BitUuid;
short128BitUuid += char(0x02); // nBytes = 2
short128BitUuid += char(0x07); // ADType128BitUuidComplete
short128BitUuid += char(0xAB); // only 1 data byte instead of 16
QTest::newRow("128bit_uuid_too_short") << short128BitUuid << 0 << false << false;
// -----------------------------------------------------------------------
// Issue 2b – 32-bit UUID AD type with only 1 data byte (nBytes = 2).
//
// ADType32BitUuidComplete requires 4 bytes of data, so we'll read past
// the end of the buffer without a proper length check.
//
// After the fix (per-case guard: if (nBytes >= 5)), no UUID is produced.
// -----------------------------------------------------------------------
QByteArray short32BitUuid;
short32BitUuid += char(0x02); // nBytes = 2
short32BitUuid += char(0x05); // ADType32BitUuidComplete
short32BitUuid += char(0xAB); // only 1 data byte instead of 4
QTest::newRow("32bit_uuid_too_short") << short32BitUuid << 0 << false << false;
// -----------------------------------------------------------------------
// Issue 2c – 16-bit UUID AD type with no data bytes (nBytes = 1).
//
// ADType16BitUuidComplete requires 2 bytes of data, so we'll read past
// the end of the buffer without a proper length check.
//
// After the fix (per-case guard: if (nBytes >= 3)), no UUID is produced.
// -----------------------------------------------------------------------
QByteArray short16BitUuid;
short16BitUuid += char(0x01); // nBytes = 1
short16BitUuid += char(0x03); // ADType16BitUuidComplete
// no data bytes — buffer ends here
QTest::newRow("16bit_uuid_too_short") << short16BitUuid << 0 << false << false;
}
#ifdef Q_OS_ANDROID
// Allocates a JVM byte array from a QByteArray. The caller is responsible for
// releasing the returned local reference via env->DeleteLocalRef().
static jbyteArray toJByteArray(QJniEnvironment &env, const QByteArray &data)
{
jbyteArray arr = env->NewByteArray(jsize(data.size()));
if (arr && !data.isEmpty())
env->SetByteArrayRegion(arr, 0, jsize(data.size()),
reinterpret_cast<const jbyte *>(data.constData()));
return arr;
}
#endif
void tst_QBluetoothDeviceDiscoveryAgent::tst_androidScanRecordParsing()
{
#ifdef Q_OS_ANDROID
QFETCH(QByteArray, scanRecord);
QFETCH(int, expectedServiceUuidCount);
QFETCH(bool, expectName);
QFETCH(bool, expectManufacturerData);
QBluetoothDeviceInfo info{ QBluetoothAddress("00:11:22:33:44:55"_L1), QString(), 0 };
QJniEnvironment env;
jbyteArray jScanRecord = toJByteArray(env, scanRecord);
parseScanRecord(info, jScanRecord);
env->DeleteLocalRef(jScanRecord);
QCOMPARE(info.serviceUuids().size(), expectedServiceUuidCount);
if (expectedServiceUuidCount > 0) {
// The valid-record row carries UUID 0x1234 — verify it was parsed correctly.
QVERIFY(info.serviceUuids().contains(QBluetoothUuid(quint16(0x1234))));
}
QCOMPARE(!info.name().isEmpty(), expectName);
if (expectManufacturerData) {
// The valid-record row carries manufacturer data for company 0x00E0.
const auto mfrData = info.manufacturerData();
QVERIFY(mfrData.contains(0x00E0));
QCOMPARE(mfrData.value(0x00E0), QByteArray::fromHex("ABCD"));
}
#endif
}
namespace {
enum class CoInitType {
Auto,
ManualApartment,
ManualMultithread,
};
} // namespace
void tst_QBluetoothDeviceDiscoveryAgent::tst_windowsDiscoveryInThread_data()
{
#ifndef Q_OS_WIN
QSKIP("This is a Windows only test");
#endif
QTest::addColumn<CoInitType>("coInitType");
QTest::newRow("default_CoInit") << CoInitType::Auto;
QTest::newRow("manual_multithreaded_CoInit") << CoInitType::ManualMultithread;
QTest::newRow("manual_apartment_CoInit") << CoInitType::ManualApartment;
}
class HelperContextObject : public QObject
{
Q_OBJECT
public slots:
void onDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error err)
{
m_error = err;
}
void onFinished()
{
m_finished = true;
}
void onCanceled()
{
m_canceled = true;
}
public:
QBluetoothDeviceDiscoveryAgent::Error m_error = QBluetoothDeviceDiscoveryAgent::NoError;
bool m_finished = false;
bool m_canceled = false;
};
void tst_QBluetoothDeviceDiscoveryAgent::tst_windowsDiscoveryInThread()
{
#ifdef Q_OS_WIN
// This test checks that a Bluetooth device can be created in a secondary
// thread. The manualCoInit flag is used to emulate the situation when
// the secondary thread already called CoInitializeEx() with a different
// threading model.
QBluetoothLocalDevice localDevice;
QBluetoothAddress address = localDevice.address();
if (address.isNull())
QSKIP("No local Bluetooth adapter found!");
QFETCH(const CoInitType, coInitType);
HelperContextObject context;
using namespace std::chrono_literals;
static constexpr auto DiscoveryTimeout = 10000ms;
auto threadFunc = [coInitType, address, &context] {
switch (coInitType) {
case CoInitType::Auto:
break; // nothing to do
case CoInitType::ManualApartment:
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
break;
case CoInitType::ManualMultithread:
CoInitializeEx(NULL, COINIT_MULTITHREADED);
break;
}
auto guard = qScopeGuard([coInitType]{
if (coInitType != CoInitType::Auto)
CoUninitialize();
});
QBluetoothDeviceDiscoveryAgent discoveryAgent(address);
QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished,
&context, &HelperContextObject::onFinished);
QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled,
&context, &HelperContextObject::onCanceled);
QObject::connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred,
&context, &HelperContextObject::onDiscoveryError);
discoveryAgent.start();
QTest::qWait(DiscoveryTimeout);
discoveryAgent.stop();
};
auto thread = std::unique_ptr<QThread>(QThread::create(std::move(threadFunc)));
thread->start();
QTRY_VERIFY_WITH_TIMEOUT(context.m_canceled || context.m_finished, DiscoveryTimeout * 2);
QCOMPARE(context.m_error, QBluetoothDeviceDiscoveryAgent::NoError);
#endif // Q_OS_WIN
}
QTEST_MAIN(tst_QBluetoothDeviceDiscoveryAgent)
#include "tst_qbluetoothdevicediscoveryagent.moc"