/****************************************************************************
**
** 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 <QtCore/QLoggingCategory>
#include "qbluetoothdevicediscoveryagent.h"
#include "qbluetoothdevicediscoveryagent_p.h"
#include "qbluetoothaddress.h"
#include "qbluetoothuuid.h"

#include "bluez/manager_p.h"
#include "bluez/adapter_p.h"
#include "bluez/device_p.h"
#include "bluez/bluez5_helper_p.h"
#include "bluez/objectmanager_p.h"
#include "bluez/adapter1_bluez5_p.h"
#include "bluez/device1_bluez5_p.h"
#include "bluez/properties_p.h"
#include "bluez/bluetoothmanagement_p.h"

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)

QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(
    const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
    lastError(QBluetoothDeviceDiscoveryAgent::NoError),
    m_adapterAddress(deviceAdapter),
    pendingCancel(false),
    pendingStart(false),
    useExtendedDiscovery(false),
    lowEnergySearchTimeout(-1), // remains -1 on BlueZ 4 -> timeout not supported
    q_ptr(parent)
{
    if (isBluez5()) {
        lowEnergySearchTimeout = 20000;
        managerBluez5 = new OrgFreedesktopDBusObjectManagerInterface(
                                           QStringLiteral("org.bluez"),
                                           QStringLiteral("/"),
                                           QDBusConnection::systemBus(), parent);
        QObject::connect(managerBluez5,
                         &OrgFreedesktopDBusObjectManagerInterface::InterfacesAdded,
                         q_ptr,
                         [this](const QDBusObjectPath &objectPath, InterfaceList interfacesAndProperties) {
            this->_q_InterfacesAdded(objectPath, interfacesAndProperties);
        });

        // start private address monitoring
        BluetoothManagement::instance();
    } else {
        manager = new OrgBluezManagerInterface(QStringLiteral("org.bluez"), QStringLiteral("/"),
                                           QDBusConnection::systemBus(), parent);
        QObject::connect(&extendedDiscoveryTimer,
                         &QTimer::timeout, q_ptr, [this]() {
            this->_q_extendedDeviceDiscoveryTimeout();
        });
        extendedDiscoveryTimer.setInterval(10000);
        extendedDiscoveryTimer.setSingleShot(true);
    }
    inquiryType = QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry;
}

QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
{
    delete adapter;
    delete adapterBluez5;
}

//TODO: Qt6 remove the pendingCancel/pendingStart logic as it is cumbersome.
//      It is a behavior change across all platforms and was initially done
//      for Bluez. The behavior should be similar to QBluetoothServiceDiscoveryAgent
//      PendingCancel creates issues whereby the agent is still shutting down
//      but isActive() below already returns false. This means the isActive() is
//      out of sync with the finished() and cancel() signal.

bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
{
    if (pendingStart)
        return true;
    if (pendingCancel)
        return false; //TODO Qt6: remove pending[Cancel|Start] logic (see comment above)

    return (adapter || adapterBluez5);
}

QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
{
    return (ClassicMethod | LowEnergyMethod);
}

void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
{
    // Currently both BlueZ backends do not distinguish discovery methods.
    // The DBus API's always return both device types. Therefore we ignore
    // the passed in methods.

    if (pendingCancel == true) {
        pendingStart = true;
        return;
    }

    discoveredDevices.clear();
    devicesProperties.clear();

    if (managerBluez5) {
        startBluez5(methods);
        return;
    }

    QDBusPendingReply<QDBusObjectPath> reply;

    if (m_adapterAddress.isNull())
        reply = manager->DefaultAdapter();
    else
        reply = manager->FindAdapter(m_adapterAddress.toString());
    reply.waitForFinished();

    if (reply.isError()) {
        errorString = reply.error().message();
        qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "ERROR: " << errorString;
        lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
        Q_Q(QBluetoothDeviceDiscoveryAgent);
        emit q->error(lastError);
        return;
    }

    adapter = new OrgBluezAdapterInterface(QStringLiteral("org.bluez"), reply.value().path(),
                                           QDBusConnection::systemBus());

    Q_Q(QBluetoothDeviceDiscoveryAgent);
    QObject::connect(adapter, &OrgBluezAdapterInterface::DeviceFound,
                     q, [this](const QString &address, const QVariantMap &dict) {
        this->_q_deviceFound(address, dict);
    });
    QObject::connect(adapter, &OrgBluezAdapterInterface::PropertyChanged,
                     q, [this](const QString &name, const QDBusVariant &value) {
        this->_q_propertyChanged(name, value);
    });

    QDBusPendingReply<QVariantMap> propertiesReply = adapter->GetProperties();
    propertiesReply.waitForFinished();
    if (propertiesReply.isError()) {
        errorString = propertiesReply.error().message();
        delete adapter;
        adapter = nullptr;
        qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "ERROR: " << errorString;
        lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
        Q_Q(QBluetoothDeviceDiscoveryAgent);
        delete adapter;
        adapter = nullptr;
        emit q->error(lastError);
        return;
    }

    if (!propertiesReply.value().value(QStringLiteral("Powered")).toBool()) {
        qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter";
        lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
        errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
        delete adapter;
        adapter = nullptr;
        emit q->error(lastError);
        return;
    }

    if (propertiesReply.value().value(QStringLiteral("Discovering")).toBool()) {
        /*  The discovery session is already ongoing. BTLE devices are advertised
            immediately after the start of the device discovery session. Hence if the
            session is already ongoing, we have just missed the BTLE device
            advertisement.

            This always happens during the second device discovery run in
            the current process. The first discovery doesn't have this issue.
            As to why the discovery session remains active despite the previous one
            being terminated is not known. This may be a bug in Bluez4.

            To workaround this issue we have to wait for two discovery
            sessions cycles.
        */
        qCDebug(QT_BT_BLUEZ) << "Using BTLE device discovery workaround.";
        useExtendedDiscovery = true;
    } else {
        useExtendedDiscovery = false;
    }

    QDBusPendingReply<> discoveryReply = adapter->StartDiscovery();
    discoveryReply.waitForFinished();
    if (discoveryReply.isError()) {
        delete adapter;
        adapter = nullptr;
        errorString = discoveryReply.error().message();
        lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
        Q_Q(QBluetoothDeviceDiscoveryAgent);
        qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "ERROR: " << errorString;
        emit q->error(lastError);
        return;
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::startBluez5(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);

    bool ok = false;
    const QString adapterPath = findAdapterForAddress(m_adapterAddress, &ok);
    if (!ok || adapterPath.isEmpty()) {
        qCWarning(QT_BT_BLUEZ) << "Cannot find Bluez 5 adapter for device search" << ok;
        lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
        errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot find valid Bluetooth adapter.");
        q->error(lastError);
        return;
    }

    adapterBluez5 = new OrgBluezAdapter1Interface(QStringLiteral("org.bluez"),
                                                  adapterPath,
                                                  QDBusConnection::systemBus());

    if (!adapterBluez5->powered()) {
        qCDebug(QT_BT_BLUEZ) << "Aborting device discovery due to offline Bluetooth Adapter";
        lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
        errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
        delete adapterBluez5;
        adapterBluez5 = nullptr;
        emit q->error(lastError);
        return;
    }

    QVariantMap map;
    if (methods == (QBluetoothDeviceDiscoveryAgent::LowEnergyMethod|QBluetoothDeviceDiscoveryAgent::ClassicMethod))
        map.insert(QStringLiteral("Transport"), QStringLiteral("auto"));
    else if (methods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)
        map.insert(QStringLiteral("Transport"), QStringLiteral("le"));
    else
        map.insert(QStringLiteral("Transport"), QStringLiteral("bredr"));

    // older BlueZ 5.x versions don't have this function
    // filterReply returns UnknownMethod which we ignore
    QDBusPendingReply<> filterReply = adapterBluez5->SetDiscoveryFilter(map);
    filterReply.waitForFinished();
    if (filterReply.isError()) {
        if (filterReply.error().type() == QDBusError::Other
                    && filterReply.error().name() == QStringLiteral("org.bluez.Error.Failed")) {
            qCDebug(QT_BT_BLUEZ) << "Discovery method" << methods << "not supported";
            lastError = QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod;
            errorString = QBluetoothDeviceDiscoveryAgent::tr("One or more device discovery methods "
                                                             "are not supported on this platform");
            delete adapterBluez5;
            adapterBluez5 = nullptr;
            emit q->error(lastError);
            return;
        } else if (filterReply.error().type() != QDBusError::UnknownMethod) {
            qCDebug(QT_BT_BLUEZ) << "SetDiscoveryFilter failed:" << filterReply.error();
        }
    }

    QtBluezDiscoveryManager::instance()->registerDiscoveryInterest(adapterBluez5->path());
    QObject::connect(QtBluezDiscoveryManager::instance(), &QtBluezDiscoveryManager::discoveryInterrupted,
                     q, [this](const QString &path){
        this->_q_discoveryInterrupted(path);
    });
    OrgFreedesktopDBusPropertiesInterface *prop = new OrgFreedesktopDBusPropertiesInterface(
                QStringLiteral("org.bluez"), QStringLiteral(""), QDBusConnection::systemBus());
    QObject::connect(prop, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
                     q, [this](const QString &interface, const QVariantMap &changedProperties,
                     const QStringList &invalidatedProperties,
                     const QDBusMessage &signal) {
        this->_q_PropertiesChanged(interface, signal.path(), changedProperties, invalidatedProperties);
    });

    // remember what we have to cleanup
    propertyMonitors.append(prop);

    // collect initial set of information
    QDBusPendingReply<ManagedObjectList> reply = managerBluez5->GetManagedObjects();
    reply.waitForFinished();
    if (!reply.isError()) {
        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")) {

                    if (path.path().indexOf(adapterBluez5->path()) != 0)
                        continue; //devices whose path doesn't start with same path we skip

                    deviceFoundBluez5(path.path(), jt.value());
                    if (!isActive()) // Can happen if stop() was called from a slot in user code.
                      return;
                }
            }
        }
    }

    // wait interval and sum up what was found
    if (!discoveryTimer) {
        discoveryTimer = new QTimer(q);
        discoveryTimer->setSingleShot(true);
        QObject::connect(discoveryTimer, &QTimer::timeout,
                         q, [this]() {
            this->_q_discoveryFinished();
        });
    }

    if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
        discoveryTimer->setInterval(lowEnergySearchTimeout);
        discoveryTimer->start();
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::stop()
{
    if (!adapter && !adapterBluez5)
        return;

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO;
    pendingCancel = true;
    pendingStart = false;
    if (adapter) {
        QDBusPendingReply<> reply = adapter->StopDiscovery();
        reply.waitForFinished();
    } else {
        _q_discoveryFinished();
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_deviceFound(const QString &address,
                                                           const QVariantMap &dict)
{
    const QBluetoothAddress btAddress(address);
    const QString btName = dict.value(QStringLiteral("Name")).toString();
    quint32 btClass = dict.value(QStringLiteral("Class")).toUInt();

    qCDebug(QT_BT_BLUEZ) << "Discovered: " << address << btName
                         << "Num UUIDs" << dict.value(QStringLiteral("UUIDs")).toStringList().count()
                         << "total device" << discoveredDevices.count() << "cached"
                         << dict.value(QStringLiteral("Cached")).toBool()
                         << "RSSI" << dict.value(QStringLiteral("RSSI")).toInt();

    QBluetoothDeviceInfo device(btAddress, btName, btClass);
    if (dict.value(QStringLiteral("RSSI")).isValid())
        device.setRssi(dict.value(QStringLiteral("RSSI")).toInt());
    QVector<QBluetoothUuid> uuids;
    const QStringList uuidStrings
            = dict.value(QLatin1String("UUIDs")).toStringList();
    for (const QString &u : uuidStrings)
        uuids.append(QBluetoothUuid(u));
    device.setServiceUuids(uuids);
    device.setCached(dict.value(QStringLiteral("Cached")).toBool());


    /*
     * Bluez v4.1 does not have extra bit which gives information if device is Bluetooth
     * Low Energy device and the way to discover it is with Class property of the Bluetooth device.
     * Low Energy devices do not have property Class.
     */
    if (btClass == 0)
        device.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
    else
        device.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
    for (int i = 0; i < discoveredDevices.size(); i++) {
        if (discoveredDevices[i].address() == device.address()) {
            if (discoveredDevices[i] == device) {
                qCDebug(QT_BT_BLUEZ) << "Duplicate: " << address;
                return;
            }
            discoveredDevices.replace(i, device);
            Q_Q(QBluetoothDeviceDiscoveryAgent);
            qCDebug(QT_BT_BLUEZ) << "Updated: " << address;

            emit q->deviceDiscovered(device);
            return; // this works if the list doesn't contain duplicates. Don't let it.
        }
    }
    qCDebug(QT_BT_BLUEZ) << "Emit: " << address;
    discoveredDevices.append(device);
    Q_Q(QBluetoothDeviceDiscoveryAgent);
    emit q->deviceDiscovered(device);
}

// Returns invalid QBluetoothDeviceInfo in case of error
static QBluetoothDeviceInfo createDeviceInfoFromBluez5Device(const QVariantMap& properties)
{
    const QBluetoothAddress btAddress(properties[QStringLiteral("Address")].toString());
    if (btAddress.isNull())
        return QBluetoothDeviceInfo();

    const QString btName = properties[QStringLiteral("Alias")].toString();
    quint32 btClass = properties[QStringLiteral("Class")].toUInt();

    QBluetoothDeviceInfo deviceInfo(btAddress, btName, btClass);
    deviceInfo.setRssi(qvariant_cast<short>(properties[QStringLiteral("RSSI")]));

    QVector<QBluetoothUuid> uuids;
    bool foundLikelyLowEnergyUuid = false;
    const QStringList foundUuids = qvariant_cast<QStringList>(properties[QStringLiteral("UUIDs")]);
    for (const auto &u: foundUuids) {
        const QBluetoothUuid id(u);
        if (id.isNull())
            continue;

        if (!foundLikelyLowEnergyUuid) {
            //once we found one BTLE service we are done
            bool ok = false;
            quint16 shortId = id.toUInt16(&ok);
            if (ok && ((shortId & QBluetoothUuid::GenericAccess) == QBluetoothUuid::GenericAccess))
                foundLikelyLowEnergyUuid = true;
        }
        uuids.append(id);
    }
    deviceInfo.setServiceUuids(uuids);

    if (!btClass) {
        deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
    } else {
        deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
        if (foundLikelyLowEnergyUuid)
            deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration);
    }

    const ManufacturerDataList deviceManufacturerData = qdbus_cast<ManufacturerDataList>(properties[QStringLiteral("ManufacturerData")]);
    const QList<quint16> keys = deviceManufacturerData.keys();
    for (quint16 key : keys)
        deviceInfo.setManufacturerData(
                    key, deviceManufacturerData.value(key).variant().toByteArray());

    return deviceInfo;
}

void QBluetoothDeviceDiscoveryAgentPrivate::deviceFoundBluez5(const QString &devicePath,
                                                              const QVariantMap &properties)
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);

    if (!q->isActive())
        return;

    auto deviceAdapter = qvariant_cast<QDBusObjectPath>(properties[QStringLiteral("Adapter")]);
     if (deviceAdapter.path() != adapterBluez5->path())
         return;

    // read information
    QBluetoothDeviceInfo deviceInfo = createDeviceInfoFromBluez5Device(properties);
    if (!deviceInfo.isValid()) // no point reporting an empty address
        return;

    qCDebug(QT_BT_BLUEZ) << "Discovered: " << deviceInfo.name() << deviceInfo.address()
                         << "Num UUIDs" << deviceInfo.serviceUuids().count()
                         << "total device" << discoveredDevices.count() << "cached"
                         << "RSSI" << deviceInfo.rssi()
                         << "Num ManufacturerData" << deviceInfo.manufacturerData().size();

    // Cache the properties so we do not have to access dbus every time to get a value
    devicesProperties[devicePath] = properties;

    for (int i = 0; i < discoveredDevices.size(); i++) {
        if (discoveredDevices[i].address() == deviceInfo.address()) {
            if (lowEnergySearchTimeout > 0 && discoveredDevices[i] == deviceInfo) {
                qCDebug(QT_BT_BLUEZ) << "Duplicate: " << deviceInfo.address();
                return;
            }
            discoveredDevices.replace(i, deviceInfo);

            emit q->deviceDiscovered(deviceInfo);
            return; // this works if the list doesn't contain duplicates. Don't let it.
        }
    }

    discoveredDevices.append(deviceInfo);
    emit q->deviceDiscovered(deviceInfo);
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_propertyChanged(const QString &name,
                                                               const QDBusVariant &value)
{
    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << name << value.variant();

    if (name == QLatin1String("Discovering")) {
      if (!value.variant().toBool()) {
            Q_Q(QBluetoothDeviceDiscoveryAgent);
            if (pendingCancel && !pendingStart) {
                adapter->deleteLater();
                adapter = nullptr;

                pendingCancel = false;
                emit q->canceled();
            } else if (pendingStart) {
                adapter->deleteLater();
                adapter = nullptr;

                pendingStart = false;
                pendingCancel = false;
                // start parameter ignored since Bluez 4 doesn't distinguish them
                start(QBluetoothDeviceDiscoveryAgent::ClassicMethod
                      | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
            } else {
                 // happens when agent is created while other agent called StopDiscovery()
                if (!adapter)
                    return;

                if (useExtendedDiscovery) {
                    useExtendedDiscovery = false;
                    /* We don't use the Start/StopDiscovery combo here
                       Using this combo surppresses the BTLE device.
                    */
                    extendedDiscoveryTimer.start();
                    return;
                }

                QDBusPendingReply<> reply = adapter->StopDiscovery();
                reply.waitForFinished();
                adapter->deleteLater();
                adapter = nullptr;
                emit q->finished();
            }
        } else {
            if (extendedDiscoveryTimer.isActive())
                extendedDiscoveryTimer.stop();
        }
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_extendedDeviceDiscoveryTimeout()
{

    if (adapter) {
        adapter->deleteLater();
        adapter = nullptr;
    }
    if (isActive()) {
        Q_Q(QBluetoothDeviceDiscoveryAgent);
        emit q->finished();
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_InterfacesAdded(const QDBusObjectPath &object_path,
                                                               InterfaceList interfaces_and_properties)
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);

    if (!q->isActive())
        return;

    if (interfaces_and_properties.contains(QStringLiteral("org.bluez.Device1"))) {
        // device interfaces belonging to different adapter
        // will be filtered out by deviceFoundBluez5();
        deviceFoundBluez5(object_path.path(), interfaces_and_properties[QStringLiteral("org.bluez.Device1")]);
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryFinished()
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);

    if (discoveryTimer)
        discoveryTimer->stop();

    QtBluezDiscoveryManager::instance()->disconnect(q);
    QtBluezDiscoveryManager::instance()->unregisterDiscoveryInterest(adapterBluez5->path());

    qDeleteAll(propertyMonitors);
    propertyMonitors.clear();

    delete adapterBluez5;
    adapterBluez5 = nullptr;

    if (pendingCancel && !pendingStart) {
        pendingCancel = false;
        emit q->canceled();
    } else if (pendingStart) {
        pendingStart = false;
        pendingCancel = false;
        start(QBluetoothDeviceDiscoveryAgent::ClassicMethod
              | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
    } else {
        emit q->finished();
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_discoveryInterrupted(const QString &path)
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);

    if (!q->isActive())
        return;

    if (path == adapterBluez5->path()) {
        qCWarning(QT_BT_BLUEZ) << "Device discovery aborted due to unexpected adapter changes from another process.";

        if (discoveryTimer)
            discoveryTimer->stop();

        QtBluezDiscoveryManager::instance()->disconnect(q);
        // no need to call unregisterDiscoveryInterest since QtBluezDiscoveryManager
        // does this automatically when emitting discoveryInterrupted(QString) signal

        delete adapterBluez5;
        adapterBluez5 = nullptr;

        errorString = QBluetoothDeviceDiscoveryAgent::tr("Bluetooth adapter error");
        lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
        emit q->error(lastError);
    }
}

void QBluetoothDeviceDiscoveryAgentPrivate::_q_PropertiesChanged(const QString &interface,
                                                                 const QString &path,
                                                                 const QVariantMap &changed_properties,
                                                                 const QStringList &invalidated_properties)
{
    Q_Q(QBluetoothDeviceDiscoveryAgent);
    if (interface != QStringLiteral("org.bluez.Device1"))
        return;

    if (!devicesProperties.contains(path))
        return;

    // Update the cached properties before checking changed_properties for RSSI and ManufacturerData
    // so the cached properties are always up to date.
    QVariantMap & properties = devicesProperties[path];
    for (QVariantMap::const_iterator it = changed_properties.constBegin();
         it != changed_properties.constEnd(); ++it) {
        properties[it.key()] = it.value();
    }

    for (const QString & property : invalidated_properties)
        properties.remove(property);

    const auto info = createDeviceInfoFromBluez5Device(properties);
    if (!info.isValid())
        return;

    if (changed_properties.contains(QStringLiteral("RSSI"))
        || changed_properties.contains(QStringLiteral("ManufacturerData"))) {

        for (int i = 0; i < discoveredDevices.size(); i++) {
            if (discoveredDevices[i].address() == info.address()) {
                QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
                if (changed_properties.contains(QStringLiteral("RSSI"))) {
                    qCDebug(QT_BT_BLUEZ) << "Updating RSSI for" << info.address()
                                         << changed_properties.value(QStringLiteral("RSSI"));
                    discoveredDevices[i].setRssi(
                                changed_properties.value(QStringLiteral("RSSI")).toInt());
                    updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
                }
                if (changed_properties.contains(QStringLiteral("ManufacturerData"))) {
                    qCDebug(QT_BT_BLUEZ) << "Updating ManufacturerData for" << info.address();
                    ManufacturerDataList changedManufacturerData =
                            qdbus_cast< ManufacturerDataList >(changed_properties.value(QStringLiteral("ManufacturerData")));

                    const QList<quint16> keys = changedManufacturerData.keys();
                    bool wasNewValue = false;
                    for (quint16 key : keys) {
                        bool added = discoveredDevices[i].setManufacturerData(key, changedManufacturerData.value(key).variant().toByteArray());
                        wasNewValue = (wasNewValue || added);
                    }

                    if (wasNewValue)
                        updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
                }

                if (lowEnergySearchTimeout > 0) {
                    if (discoveredDevices[i] != info) { // field other than manufacturer or rssi changed
                        if (discoveredDevices.at(i).name() == info.name()) {
                            qCDebug(QT_BT_BLUEZ) << "Almost Duplicate " << info.address()
                                                   << info.name() << "- replacing in place";
                            discoveredDevices.replace(i, info);
                            emit q->deviceDiscovered(info);
                        }
                    } else {
                        if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
                            emit q->deviceUpdated(discoveredDevices[i], updatedFields);
                    }

                    return;
                }

                discoveredDevices.replace(i, info);
                emit q_ptr->deviceDiscovered(discoveredDevices[i]);

                if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
                    emit q->deviceUpdated(discoveredDevices[i], updatedFields);
                return;
            }
        }
    }
}
QT_END_NAMESPACE
