/****************************************************************************
**
** 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 "qbluetoothservicediscoveryagent.h"
#include "qbluetoothservicediscoveryagent_p.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 <QtCore/QFile>
#include <QtCore/QLibraryInfo>
#include <QtCore/QLoggingCategory>
#include <QtCore/QProcess>
#include <QtDBus/QDBusPendingCallWatcher>
#include <QtConcurrent/QtConcurrentRun>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)

QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate(
    QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter)
:   error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive),
    mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false),
    q_ptr(qp)
{
    if (isBluez5()) {
        managerBluez5 = new OrgFreedesktopDBusObjectManagerInterface(
                                    QStringLiteral("org.bluez"), QStringLiteral("/"),
                                    QDBusConnection::systemBus());
        qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>();
    } else {
        qRegisterMetaType<ServiceMap>();
        qDBusRegisterMetaType<ServiceMap>();

        manager = new OrgBluezManagerInterface(QStringLiteral("org.bluez"), QStringLiteral("/"),
                                               QDBusConnection::systemBus());
    }
}

QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate()
{
    delete device;
    delete manager;
    delete managerBluez5;
    delete adapter;
}

void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode();

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

    QDBusPendingReply<QDBusObjectPath> reply;
    if (m_deviceAdapterAddress.isNull())
        reply = manager->DefaultAdapter();
    else
        reply = manager->FindAdapter(m_deviceAdapterAddress.toString());

    reply.waitForFinished();
    if (reply.isError()) {
        error = QBluetoothServiceDiscoveryAgent::InputOutputError;
        errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to find appointed local adapter");
        emit q->error(error);
        _q_serviceDiscoveryFinished();
        return;
    }

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

    if (m_deviceAdapterAddress.isNull()) {
        QDBusPendingReply<QVariantMap> reply = adapter->GetProperties();
        reply.waitForFinished();
        if (!reply.isError()) {
            const QBluetoothAddress path_address(reply.value().value(QStringLiteral("Address")).toString());
            m_deviceAdapterAddress = path_address;
        }
    }

    QDBusPendingReply<QDBusObjectPath> deviceObjectPath = adapter->FindDevice(address.toString());

    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(deviceObjectPath, q);
    watcher->setProperty("_q_BTaddress", QVariant::fromValue(address));
    QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
                     q, [this](QDBusPendingCallWatcher *watcher){
        this->_q_foundDevice(watcher);
    });
}

// Bluez 5
void QBluetoothServiceDiscoveryAgentPrivate::startBluez5(const QBluetoothAddress &address)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    if (foundHostAdapterPath.isEmpty()) {
        // check that we match adapter addresses or use first if it wasn't specified

        bool ok = false;
        foundHostAdapterPath  = findAdapterForAddress(m_deviceAdapterAddress, &ok);
        if (!ok) {
            discoveredDevices.clear();
            error = QBluetoothServiceDiscoveryAgent::InputOutputError;
            errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot access adapter during service discovery");
            emit q->error(error);
            _q_serviceDiscoveryFinished();
            return;
        }

        if (foundHostAdapterPath.isEmpty()) {
            // Cannot find a local adapter
            // Abort any outstanding discoveries
            discoveredDevices.clear();

            error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError;
            errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot find local Bluetooth adapter");
            emit q->error(error);
            _q_serviceDiscoveryFinished();

            return;
        }
    }

    // ensure we didn't go offline yet
    OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez"),
                                      foundHostAdapterPath, QDBusConnection::systemBus());
    if (!adapter.powered()) {
        discoveredDevices.clear();

        error = QBluetoothServiceDiscoveryAgent::PoweredOffError;
        errorString = QBluetoothServiceDiscoveryAgent::tr("Local device is powered off");
        emit q->error(error);

        _q_serviceDiscoveryFinished();
        return;
    }

    if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) {
        performMinimalServiceDiscovery(address);
    } else {
        runExternalSdpScan(address, QBluetoothAddress(adapter.address()));
    }
}

/* Bluez 5
 * src/tools/sdpscanner performs an SDP scan. This is
 * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2.
 */
void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan(
        const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    if (!sdpScannerProcess) {
        const QString binPath = QLibraryInfo::location(QLibraryInfo::BinariesPath);
        QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner"));
        if (!fileInfo.exists() || !fileInfo.isExecutable()) {
            _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError,
                             QBluetoothServiceDiscoveryAgent::tr("Unable to find sdpscanner"),
                             QStringList());
            qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:"
                                   << fileInfo.canonicalFilePath();
            return;
        }

        sdpScannerProcess = new QProcess(q);
        sdpScannerProcess->setReadChannel(QProcess::StandardOutput);
        if (QT_BT_BLUEZ().isDebugEnabled())
            sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel);
        sdpScannerProcess->setProgram(fileInfo.canonicalFilePath());
        q->connect(sdpScannerProcess,
                   QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
                   q, [this](int exitCode, QProcess::ExitStatus status){
            this->_q_sdpScannerDone(exitCode, status);
        });
    }

    QStringList arguments;
    arguments << remoteAddress.toString() << localAddress.toString();

    // No filter implies PUBLIC_BROWSE_GROUP based SDP scan
    if (!uuidFilter.isEmpty()) {
        arguments << QLatin1String("-u"); // cmd line option for list of uuids
        for (const QBluetoothUuid& uuid : qAsConst(uuidFilter))
            arguments << uuid.toString();
    }

    sdpScannerProcess->setArguments(arguments);
    sdpScannerProcess->start();
}

// Bluez 5
void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status)
{
    if (status != QProcess::NormalExit || exitCode != 0) {
        qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode;
        if (singleDevice) {
            _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError,
                             QBluetoothServiceDiscoveryAgent::tr("Unable to perform SDP scan"),
                             QStringList());
        } else {
            // go to next device
            _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), QStringList());
        }
        return;
    }

    QStringList xmlRecords;
    const QByteArray output = sdpScannerProcess->readAllStandardOutput();
    const QString decodedData = QString::fromUtf8(QByteArray::fromBase64(output));

    // split the various xml docs up
    int next;
    int start = decodedData.indexOf(QStringLiteral("<?xml"), 0);
    if (start != -1) {
        do {
            next = decodedData.indexOf(QStringLiteral("<?xml"), start + 1);
            if (next != -1)
                xmlRecords.append(decodedData.mid(start, next-start));
            else
                xmlRecords.append(decodedData.mid(start, decodedData.size() - start));
            start = next;
        } while ( start != -1);
    }

    _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), xmlRecords);
}

// Bluez 5
void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode,
                                                              const QString &errorDescription,
                                                              const QStringList &xmlRecords)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) {
        qCWarning(QT_BT_BLUEZ) << "SDP search failed for"
                              << (!discoveredDevices.isEmpty()
                                     ? discoveredDevices.at(0).address().toString()
                                     : QStringLiteral("<Unknown>"));
        // We have an error which we need to indicate and stop further processing
        discoveredDevices.clear();
        error = errorCode;
        errorString = errorDescription;
        emit q->error(error);
    } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) {
        for (const QString &record : xmlRecords) {
            QBluetoothServiceInfo serviceInfo = parseServiceXml(record);

            //apply uuidFilter
            if (!uuidFilter.isEmpty()) {
                bool serviceNameMatched = uuidFilter.contains(serviceInfo.serviceUuid());
                bool serviceClassMatched = false;
                const QList<QBluetoothUuid> serviceClassUuids
                        = serviceInfo.serviceClassUuids();
                for (const QBluetoothUuid &id : serviceClassUuids) {
                    if (uuidFilter.contains(id)) {
                        serviceClassMatched = true;
                        break;
                    }
                }

                if (!serviceNameMatched && !serviceClassMatched)
                    continue;
            }

            if (!serviceInfo.isValid())
                continue;

            // Bluez sdpscanner declares custom uuids into the service class uuid list.
            // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids()
            // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid
            const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids();
            for (const QBluetoothUuid &id : serviceClassUuids) {
                if (id.minimumSize() == 16) {
                    serviceInfo.setServiceUuid(id);
                    if (serviceInfo.serviceName().isEmpty()) {
                        serviceInfo.setServiceName(
                                    QBluetoothServiceDiscoveryAgent::tr("Custom Service"));
                    }
                    QBluetoothServiceInfo::Sequence modSeq =
                            serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>();
                    modSeq.removeOne(QVariant::fromValue(id));
                    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, modSeq);
                    break;
                }
            }

            if (!isDuplicatedService(serviceInfo)) {
                discoveredServices.append(serviceInfo);
                qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString()
                                     << serviceInfo.serviceName() << serviceInfo.serviceUuid()
                                     << ">>>" << serviceInfo.serviceClassUuids();
                // Use queued connection to allow us finish the service looping; the application
                // might call stop() when it has detected the service-of-interest.
                QMetaObject::invokeMethod(q, "serviceDiscovered", Qt::QueuedConnection,
                                          Q_ARG(QBluetoothServiceInfo, serviceInfo));
            }
        }
    }

    _q_serviceDiscoveryFinished();
}

void QBluetoothServiceDiscoveryAgentPrivate::stop()
{
    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called";
    if (device) {
        //we are waiting for _q_discoveredServices() to be called
        // adapter is already 0
        QDBusPendingReply<> reply = device->CancelDiscovery();
        reply.waitForFinished();

        device->deleteLater();
        device = nullptr;
        Q_ASSERT(!adapter);
    } else if (adapter) {
        //we are waiting for _q_createdDevice() to be called
        adapter->deleteLater();
        adapter = nullptr;
        Q_ASSERT(!device);
    }


    discoveredDevices.clear();
    setDiscoveryState(Inactive);

    // must happen after discoveredDevices.clear() above to avoid retrigger of next scan
    // while waitForFinished() is waiting
    if (sdpScannerProcess) { // Bluez 5
        if (sdpScannerProcess->state() != QProcess::NotRunning) {
            sdpScannerProcess->kill();
            sdpScannerProcess->waitForFinished();
        }
    }

    Q_Q(QBluetoothServiceDiscoveryAgent);
    emit q->canceled();
}

void QBluetoothServiceDiscoveryAgentPrivate::_q_foundDevice(QDBusPendingCallWatcher *watcher)
{
    if (!adapter) {
        watcher->deleteLater();
        return;
    }

    Q_Q(QBluetoothServiceDiscoveryAgent);

    const QBluetoothAddress &address = watcher->property("_q_BTaddress").value<QBluetoothAddress>();

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "found" << address.toString();

    QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher;
    watcher->deleteLater();
    if (deviceObjectPath.isError()) {
        if (deviceObjectPath.error().name() != QStringLiteral("org.bluez.Error.DoesNotExist")) {
            qCDebug(QT_BT_BLUEZ) << "Find device failed Error: " << error << deviceObjectPath.error().name();
            delete adapter;
            adapter = nullptr;
            if (singleDevice) {
                error = QBluetoothServiceDiscoveryAgent::InputOutputError;
                errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device");
                emit q->error(error);
            }
            _q_serviceDiscoveryFinished();
            return;
        }

        deviceObjectPath = adapter->CreateDevice(address.toString());
        watcher = new QDBusPendingCallWatcher(deviceObjectPath, q);
        watcher->setProperty("_q_BTaddress",  QVariant::fromValue(address));
        QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
                         q, [this](QDBusPendingCallWatcher *watcher){
            this->_q_createdDevice(watcher);
        });

        return;
    }

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path();
    discoverServices(deviceObjectPath.value().path());
}

void QBluetoothServiceDiscoveryAgentPrivate::_q_createdDevice(QDBusPendingCallWatcher *watcher)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    if (!adapter) {
        watcher->deleteLater();
        return;
    }

    const QBluetoothAddress &address = watcher->property("_q_BTaddress").value<QBluetoothAddress>();

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "created" << address.toString();

    QDBusPendingReply<QDBusObjectPath> deviceObjectPath = *watcher;
    watcher->deleteLater();
    if (deviceObjectPath.isError()) {
        if (deviceObjectPath.error().name() != QLatin1String("org.bluez.Error.AlreadyExists")) {
            qCDebug(QT_BT_BLUEZ) << "Create device failed Error: " << error << deviceObjectPath.error().name();
            delete adapter;
            adapter = nullptr;
            if (singleDevice) {
                error = QBluetoothServiceDiscoveryAgent::InputOutputError;
                errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device");
                emit q->error(error);
            }
            _q_serviceDiscoveryFinished();
            return;
        }
    }

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "path" << deviceObjectPath.value().path();
    discoverServices(deviceObjectPath.value().path());
}

void QBluetoothServiceDiscoveryAgentPrivate::discoverServices(const QString &deviceObjectPath)
{
    Q_Q(QBluetoothServiceDiscoveryAgent);

    device = new OrgBluezDeviceInterface(QStringLiteral("org.bluez"),
                                         deviceObjectPath,
                                         QDBusConnection::systemBus());
    delete adapter;
    adapter = nullptr;

    QVariantMap deviceProperties;
    QString classType;
    QDBusPendingReply<QVariantMap> deviceReply = device->GetProperties();
    deviceReply.waitForFinished();
    if (!deviceReply.isError()) {
        deviceProperties = deviceReply.value();
        classType = deviceProperties.value(QStringLiteral("Class")).toString();
    }

    /*
     * Low Energy services in bluez are represented as the list of the paths that can be
     * accessed with org.bluez.Characteristic
     */
    //QDBusArgument services = v.value(QLatin1String("Services")).value<QDBusArgument>();


    /*
     * 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.
     * In case we have LE device finish service discovery; otherwise search for regular services.
     */
    if (classType.isEmpty()) { //is BLE device or device properties above not retrievable
        qCDebug(QT_BT_BLUEZ) << "Discovered BLE-only device. Normal service discovery skipped.";
        delete device;
        device = nullptr;

        const QStringList deviceUuids = deviceProperties.value(QStringLiteral("UUIDs")).toStringList();
        for (int i = 0; i < deviceUuids.size(); i++) {
            QString b = deviceUuids.at(i);
            b = b.remove(QLatin1Char('{')).remove(QLatin1Char('}'));
            const QBluetoothUuid uuid(b);

            qCDebug(QT_BT_BLUEZ) << "Discovered service" << uuid << uuidFilter.size();
            QBluetoothServiceInfo service;
            service.setDevice(discoveredDevices.at(0));
            bool ok = false;
            quint16 serviceClass = uuid.toUInt16(&ok);
            if (ok)
                service.setServiceName(QBluetoothUuid::serviceClassToString(
                                           static_cast<QBluetoothUuid::ServiceClassUuid>(serviceClass)));

            QBluetoothServiceInfo::Sequence classId;
            classId << QVariant::fromValue(uuid);
            service.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);

            QBluetoothServiceInfo::Sequence protocolDescriptorList;
            {
                QBluetoothServiceInfo::Sequence protocol;
                protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap));
                protocolDescriptorList.append(QVariant::fromValue(protocol));
            }
            {
                QBluetoothServiceInfo::Sequence protocol;
                protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att));
                protocolDescriptorList.append(QVariant::fromValue(protocol));
            }
            service.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList);

            if (uuidFilter.isEmpty())
                emit q->serviceDiscovered(service);
            else {
                for (int j = 0; j < uuidFilter.size(); j++) {
                    if (uuidFilter.at(j) == uuid)
                        emit q->serviceDiscovered(service);
                }
            }
        }

        if (singleDevice && deviceReply.isError()) {
            error = QBluetoothServiceDiscoveryAgent::InputOutputError;
            errorString = QBluetoothServiceDiscoveryAgent::tr("Unable to access device");
            emit q->error(error);
        }
        _q_serviceDiscoveryFinished();
    } else {
        QString pattern;
        for (const QBluetoothUuid &uuid : qAsConst(uuidFilter))
            pattern += uuid.toString().remove(QLatin1Char('{')).remove(QLatin1Char('}')) + QLatin1Char(' ');

        pattern = pattern.trimmed();
        qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Discover restrictions:" << pattern;

        QDBusPendingReply<ServiceMap> discoverReply = device->DiscoverServices(pattern);
        QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(discoverReply, q);
        QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
                         q, [this](QDBusPendingCallWatcher *watcher){
            this->_q_discoveredServices(watcher);
        });
    }
}

// Bluez 4
void QBluetoothServiceDiscoveryAgentPrivate::_q_discoveredServices(QDBusPendingCallWatcher *watcher)
{
    if (!device) {
        watcher->deleteLater();
        return;
    }

    qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO;
    Q_Q(QBluetoothServiceDiscoveryAgent);

    QDBusPendingReply<ServiceMap> reply = *watcher;
    if (reply.isError()) {
        qCDebug(QT_BT_BLUEZ) << "discoveredServices error: " << error << reply.error().message();
        watcher->deleteLater();
        if (singleDevice) {
            error = QBluetoothServiceDiscoveryAgent::UnknownError;
            errorString = reply.error().message();
            emit q->error(error);
        }
        delete device;
        device = nullptr;
        _q_serviceDiscoveryFinished();
        return;
    }

    const ServiceMap map = reply.value();

    qCDebug(QT_BT_BLUEZ) << "Parsing xml" << discoveredDevices.at(0).address().toString() << discoveredDevices.count() << map.count();



    for (const QString &record : map) {
        QBluetoothServiceInfo serviceInfo = parseServiceXml(record);

        if (!serviceInfo.isValid())
            continue;

        // Don't need to apply uuidFilter because Bluez 4 applies
        // search pattern during DiscoverServices() call

        Q_Q(QBluetoothServiceDiscoveryAgent);
        // Some service uuids are unknown to Bluez. In such cases we fall back
        // to our own naming resolution.
        if (serviceInfo.serviceName().isEmpty()
            && !serviceInfo.serviceClassUuids().isEmpty()) {
            const QList<QBluetoothUuid> classUuids = serviceInfo.serviceClassUuids();
            for (const QBluetoothUuid &classUuid : classUuids) {
                bool ok = false;
                QBluetoothUuid::ServiceClassUuid clsId
                    = static_cast<QBluetoothUuid::ServiceClassUuid>(classUuid.toUInt16(&ok));
                if (ok) {
                    serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId));
                    break;
                }
            }
        }

        if (!isDuplicatedService(serviceInfo)) {
            discoveredServices.append(serviceInfo);
            qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString()
                                 << serviceInfo.serviceName();
            emit q->serviceDiscovered(serviceInfo);
        }

        // could stop discovery, check for state
        if (discoveryState() == Inactive)
            qCDebug(QT_BT_BLUEZ) << "Exit discovery after stop";
    }

    watcher->deleteLater();
    delete device;
    device = nullptr;

    _q_serviceDiscoveryFinished();
}

QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml(
                            const QString& xmlRecord)
{
    QXmlStreamReader xml(xmlRecord);

    QBluetoothServiceInfo serviceInfo;
    serviceInfo.setDevice(discoveredDevices.at(0));

    while (!xml.atEnd()) {
        xml.readNext();

        if (xml.tokenType() == QXmlStreamReader::StartElement &&
            xml.name() == QLatin1String("attribute")) {
            quint16 attributeId =
                xml.attributes().value(QLatin1String("id")).toString().toUShort(nullptr, 0);

            if (xml.readNextStartElement()) {
                const QVariant value = readAttributeValue(xml);
                serviceInfo.setAttribute(attributeId, value);
            }
        }
    }

    return serviceInfo;
}

// Bluez 5
void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress)
{
    if (foundHostAdapterPath.isEmpty()) {
        _q_serviceDiscoveryFinished();
        return;
    }

    Q_Q(QBluetoothServiceDiscoveryAgent);

    QDBusPendingReply<ManagedObjectList> reply = managerBluez5->GetManagedObjects();
    reply.waitForFinished();
    if (reply.isError()) {
        if (singleDevice) {
            error = QBluetoothServiceDiscoveryAgent::InputOutputError;
            errorString = reply.error().message();
            emit q->error(error);

        }
        _q_serviceDiscoveryFinished();
        return;
    }

    QStringList uuidStrings;

    ManagedObjectList managedObjectList = reply.value();
    for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
        const InterfaceList &ifaceList = it.value();

        for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
            const QString &iface = jt.key();
            const QVariantMap &ifaceValues = jt.value();

            if (iface == QStringLiteral("org.bluez.Device1")) {
                if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address")).toString()) {
                    uuidStrings = ifaceValues.value(QStringLiteral("UUIDs")).toStringList();
                    break;
                }
            }
        }
        if (!uuidStrings.isEmpty())
            break;
    }

    if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) {
        qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString();
         // nothing found -> go to next uuid
        _q_serviceDiscoveryFinished();
        return;
    }

    qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings;

    QBluetoothUuid uuid;
    for (int i = 0; i < uuidStrings.count(); i++) {
        uuid = QBluetoothUuid(uuidStrings.at(i));
        if (uuid.isNull())
            continue;

        //apply uuidFilter
        if (!uuidFilter.isEmpty() && !uuidFilter.contains(uuid))
            continue;

        QBluetoothServiceInfo serviceInfo;
        serviceInfo.setDevice(discoveredDevices.at(0));

        if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID
            serviceInfo.setServiceUuid(uuid);
            serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service"));
        } else {
            // set uuid as service class id
            QBluetoothServiceInfo::Sequence classId;
            classId << QVariant::fromValue(uuid);
            serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
            QBluetoothUuid::ServiceClassUuid clsId
                    = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff);
            serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId));
        }

        QBluetoothServiceInfo::Sequence protocolDescriptorList;
        {
            QBluetoothServiceInfo::Sequence protocol;
            protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap));
            protocolDescriptorList.append(QVariant::fromValue(protocol));
        }
        {
            QBluetoothServiceInfo::Sequence protocol;
            protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Att));
            protocolDescriptorList.append(QVariant::fromValue(protocol));
        }
        serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList);

        //don't include the service if we already discovered it before
        if (!isDuplicatedService(serviceInfo)) {
            discoveredServices << serviceInfo;
            qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString()
                                 << serviceInfo.serviceName();
            emit q->serviceDiscovered(serviceInfo);
        }
    }

    _q_serviceDiscoveryFinished();
}

QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml)
{
    if (xml.name() == QLatin1String("boolean")) {
        const QString value = xml.attributes().value(QStringLiteral("value")).toString();
        xml.skipCurrentElement();
        return value == QLatin1String("true");
    } else if (xml.name() == QLatin1String("uint8")) {
        quint8 value = xml.attributes().value(QStringLiteral("value")).toString().toUShort(nullptr, 0);
        xml.skipCurrentElement();
        return value;
    } else if (xml.name() == QLatin1String("uint16")) {
        quint16 value = xml.attributes().value(QStringLiteral("value")).toString().toUShort(nullptr, 0);
        xml.skipCurrentElement();
        return value;
    } else if (xml.name() == QLatin1String("uint32")) {
        quint32 value = xml.attributes().value(QStringLiteral("value")).toString().toUInt(nullptr, 0);
        xml.skipCurrentElement();
        return value;
    } else if (xml.name() == QLatin1String("uint64")) {
        quint64 value = xml.attributes().value(QStringLiteral("value")).toString().toULongLong(nullptr, 0);
        xml.skipCurrentElement();
        return value;
    } else if (xml.name() == QLatin1String("uuid")) {
        QBluetoothUuid uuid;
        const QString value = xml.attributes().value(QStringLiteral("value")).toString();
        if (value.startsWith(QStringLiteral("0x"))) {
            if (value.length() == 6) {
                quint16 v = value.toUShort(nullptr, 0);
                uuid = QBluetoothUuid(v);
            } else if (value.length() == 10) {
                quint32 v = value.toUInt(nullptr, 0);
                uuid = QBluetoothUuid(v);
            }
        } else {
            uuid = QBluetoothUuid(value);
        }
        xml.skipCurrentElement();
        return QVariant::fromValue(uuid);
    } else if (xml.name() == QLatin1String("text") || xml.name() == QLatin1String("url")) {
        QString value = xml.attributes().value(QStringLiteral("value")).toString();
        if (xml.attributes().value(QStringLiteral("encoding")) == QLatin1String("hex"))
            value = QString::fromUtf8(QByteArray::fromHex(value.toLatin1()));
        xml.skipCurrentElement();
        return value;
    } else if (xml.name() == QLatin1String("sequence")) {
        QBluetoothServiceInfo::Sequence sequence;

        while (xml.readNextStartElement()) {
            QVariant value = readAttributeValue(xml);
            sequence.append(value);
        }

        return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(sequence);
    } else {
        qCWarning(QT_BT_BLUEZ) << "unknown attribute type"
                               << xml.name().toString()
                               << xml.attributes().value(QStringLiteral("value")).toString();
        xml.skipCurrentElement();
        return QVariant();
    }
}

QT_END_NAMESPACE
