// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qlowenergycontroller_android_p.h"
#include "android/androidutils_p.h"
#include "android/jni_android_p.h"
#include <QCoreApplication>
#include <QtCore/QLoggingCategory>
#include <QtCore/QJniEnvironment>
#include <QtCore/QJniObject>
#include <QtCore/q26numeric.h>
#include <QtBluetooth/QLowEnergyServiceData>
#include <QtBluetooth/QLowEnergyCharacteristicData>
#include <QtBluetooth/QLowEnergyDescriptorData>
#include <QtBluetooth/QLowEnergyAdvertisingData>
#include <QtBluetooth/QLowEnergyAdvertisingParameters>
#include <QtBluetooth/QLowEnergyConnectionParameters>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID)

// BT Core v5.3, 3.2.9, Vol 3, Part F
const int BTLE_MAX_ATTRIBUTE_VALUE_SIZE = 512;

// Conversion: QBluetoothUuid -> java.util.UUID
static QJniObject javaUuidfromQtUuid(const QBluetoothUuid& uuid)
{
    // cut off leading and trailing brackets
    const QString output = uuid.toString(QUuid::WithoutBraces);

    QJniObject javaString = QJniObject::fromString(output);
    QJniObject javaUuid = QJniObject::callStaticMethod<QtJniTypes::UUID>(
                QtJniTypes::Traits<QtJniTypes::UUID>::className(), "fromString",
                javaString.object<jstring>());

    return javaUuid;
}

QLowEnergyControllerPrivateAndroid::QLowEnergyControllerPrivateAndroid()
    : QLowEnergyControllerPrivate(),
      hub(0)
{
    registerQLowEnergyControllerMetaType();
}

QLowEnergyControllerPrivateAndroid::~QLowEnergyControllerPrivateAndroid()
{
    if (role == QLowEnergyController::PeripheralRole) {
        if (hub)
            hub->javaObject().callMethod<void>("disconnectServer");
    }
}

void QLowEnergyControllerPrivateAndroid::init()
{
    const bool isPeripheral = (role == QLowEnergyController::PeripheralRole);

    if (isPeripheral) {
        qRegisterMetaType<QJniObject>();
        hub = new LowEnergyNotificationHub(remoteDevice, isPeripheral, this);
        // we only connect to the peripheral role specific signals
        // TODO add connections as they get added later on
        connect(hub, &LowEnergyNotificationHub::connectionUpdated,
                this, &QLowEnergyControllerPrivateAndroid::connectionUpdated);
        connect(hub, &LowEnergyNotificationHub::mtuChanged,
                this, &QLowEnergyControllerPrivateAndroid::mtuChanged);
        connect(hub, &LowEnergyNotificationHub::advertisementError,
                this, &QLowEnergyControllerPrivateAndroid::advertisementError);
        connect(hub, &LowEnergyNotificationHub::serverCharacteristicChanged,
                this, &QLowEnergyControllerPrivateAndroid::serverCharacteristicChanged);
        connect(hub, &LowEnergyNotificationHub::serverDescriptorWritten,
                this, &QLowEnergyControllerPrivateAndroid::serverDescriptorWritten);
    } else {
        hub = new LowEnergyNotificationHub(remoteDevice, isPeripheral, this);
        // we only connect to the central role specific signals
        connect(hub, &LowEnergyNotificationHub::connectionUpdated,
                this, &QLowEnergyControllerPrivateAndroid::connectionUpdated);
        connect(hub, &LowEnergyNotificationHub::mtuChanged,
                this, &QLowEnergyControllerPrivateAndroid::mtuChanged);
        connect(hub, &LowEnergyNotificationHub::servicesDiscovered,
                this, &QLowEnergyControllerPrivateAndroid::servicesDiscovered);
        connect(hub, &LowEnergyNotificationHub::serviceDetailsDiscoveryFinished,
                this, &QLowEnergyControllerPrivateAndroid::serviceDetailsDiscoveryFinished);
        connect(hub, &LowEnergyNotificationHub::characteristicRead,
                this, &QLowEnergyControllerPrivateAndroid::characteristicRead);
        connect(hub, &LowEnergyNotificationHub::descriptorRead,
                this, &QLowEnergyControllerPrivateAndroid::descriptorRead);
        connect(hub, &LowEnergyNotificationHub::characteristicWritten,
                this, &QLowEnergyControllerPrivateAndroid::characteristicWritten);
        connect(hub, &LowEnergyNotificationHub::descriptorWritten,
                this, &QLowEnergyControllerPrivateAndroid::descriptorWritten);
        connect(hub, &LowEnergyNotificationHub::characteristicChanged,
                this, &QLowEnergyControllerPrivateAndroid::characteristicChanged);
        connect(hub, &LowEnergyNotificationHub::serviceError,
                this, &QLowEnergyControllerPrivateAndroid::serviceError);
        connect(hub, &LowEnergyNotificationHub::remoteRssiRead,
                this, &QLowEnergyControllerPrivateAndroid::remoteRssiRead);
    }
}

void QLowEnergyControllerPrivateAndroid::connectToDevice()
{
    if (!hub) {
        qCCritical(QT_BT_ANDROID) << "connectToDevice() LE controller has not been initialized";
        return;
    }

    if (!ensureAndroidPermission(QBluetoothPermission::Access)) {
        // This is unlikely to happen as a valid local adapter is a precondition
        setError(QLowEnergyController::MissingPermissionsError);
        qCWarning(QT_BT_ANDROID) << "connectToDevice() failed due to missing permissions";
        return;
    }

    // required to pass unit test on default backend
    if (remoteDevice.isNull()) {
        qCWarning(QT_BT_ANDROID) << "Invalid/null remote device address";
        setError(QLowEnergyController::UnknownRemoteDeviceError);
        return;
    }

    setState(QLowEnergyController::ConnectingState);

    if (!hub->javaObject().isValid()) {
        qCWarning(QT_BT_ANDROID) << "Cannot initiate QtBluetoothLE";
        setError(QLowEnergyController::ConnectionError);
        setState(QLowEnergyController::UnconnectedState);
        return;
    }

    bool result = hub->javaObject().callMethod<jboolean>("connect");
    if (!result) {
        setError(QLowEnergyController::ConnectionError);
        setState(QLowEnergyController::UnconnectedState);
        return;
    }
}

void QLowEnergyControllerPrivateAndroid::disconnectFromDevice()
{
    /* Catch an Android timeout bug. If the device is connecting but cannot
     * physically connect it seems to ignore the disconnect call below.
     * At least BluetoothGattCallback.onConnectionStateChange never
     * arrives. The next BluetoothGatt.connect() works just fine though.
     * */

    QLowEnergyController::ControllerState oldState = state;
    setState(QLowEnergyController::ClosingState);

    if (hub) {
        if (role == QLowEnergyController::PeripheralRole)
            hub->javaObject().callMethod<void>("disconnectServer");
        else
            hub->javaObject().callMethod<void>("disconnect");
    }

    if (oldState == QLowEnergyController::ConnectingState)
        setState(QLowEnergyController::UnconnectedState);
}

void QLowEnergyControllerPrivateAndroid::discoverServices()
{
    // No need to check bluetooth permissions here as 'connected' is a precondition

    if (hub && hub->javaObject().callMethod<jboolean>("discoverServices")) {
        qCDebug(QT_BT_ANDROID) << "Service discovery initiated";
    } else {
        //revert to connected state
        setError(QLowEnergyController::NetworkError);
        setState(QLowEnergyController::ConnectedState);
    }
}

void QLowEnergyControllerPrivateAndroid::discoverServiceDetails(
        const QBluetoothUuid &service, QLowEnergyService::DiscoveryMode mode)
{
    Q_UNUSED(mode);
    if (!serviceList.contains(service)) {
        qCWarning(QT_BT_ANDROID) << "Discovery of unknown service" << service.toString()
                                 << "not possible";
        return;
    }

    if (!hub)
        return;

    QString tempUuid = service.toString(QUuid::WithoutBraces);

    QJniEnvironment env;
    QJniObject uuid = QJniObject::fromString(tempUuid);
    bool readAllValues = mode == QLowEnergyService::FullDiscovery;
    bool result = hub->javaObject().callMethod<jboolean>("discoverServiceDetails",
                                                         uuid.object<jstring>(),
                                                         readAllValues);
    if (!result) {
        QSharedPointer<QLowEnergyServicePrivate> servicePrivate =
                serviceList.value(service);
        if (!servicePrivate.isNull()) {
            servicePrivate->setError(QLowEnergyService::UnknownError);
            servicePrivate->setState(QLowEnergyService::RemoteService);
        }
        qCWarning(QT_BT_ANDROID) << "Cannot discover details for" << service.toString();
        return;
    }

    qCDebug(QT_BT_ANDROID) << "Discovery of" << service << "started";
}

void QLowEnergyControllerPrivateAndroid::writeCharacteristic(
        const QSharedPointer<QLowEnergyServicePrivate> service,
        const QLowEnergyHandle charHandle,
        const QByteArray &newValue,
        QLowEnergyService::WriteMode mode)
{
    //TODO don't ignore WriteWithResponse, right now we assume responses
    Q_ASSERT(!service.isNull());

    if (!service->characteristicList.contains(charHandle))
        return;

    QJniEnvironment env;
    const jsize nativeSize = q26::saturate_cast<jsize>(newValue.size());
    jbyteArray payload;
    payload = env->NewByteArray(nativeSize);
    env->SetByteArrayRegion(payload, 0, nativeSize,
                            (jbyte *)newValue.constData());

    bool result = false;
    if (hub) {
        if (role == QLowEnergyController::CentralRole) {
            qCDebug(QT_BT_ANDROID) << "Write characteristic with handle " << charHandle
                     << newValue.toHex() << "(service:" << service->uuid
                     << ", writeWithResponse:" << (mode == QLowEnergyService::WriteWithResponse)
                     << ", signed:" << (mode == QLowEnergyService::WriteSigned) << ")";
            result = hub->javaObject().callMethod<jboolean>("writeCharacteristic",
                                                            charHandle, payload, jint(mode));
        } else { // peripheral mode
            qCDebug(QT_BT_ANDROID) << "Write server characteristic with handle " << charHandle
                     << newValue.toHex() << "(service:" << service->uuid;

            const auto &characteristic = characteristicForHandle(charHandle);
            if (characteristic.isValid()) {
                const QJniObject charUuid = javaUuidfromQtUuid(characteristic.uuid());
                result = hub->javaObject().callMethod<jboolean>(
                            "writeCharacteristic",
                            service->androidService.object<QtJniTypes::BluetoothGattService>(),
                            charUuid.object<QtJniTypes::UUID>(), payload);
                if (result)
                    service->characteristicList[charHandle].value = newValue;
            }
        }
    }

    env->DeleteLocalRef(payload);

    if (!result)
        service->setError(QLowEnergyService::CharacteristicWriteError);
}

void QLowEnergyControllerPrivateAndroid::writeDescriptor(
        const QSharedPointer<QLowEnergyServicePrivate> service,
        const QLowEnergyHandle charHandle,
        const QLowEnergyHandle descHandle,
        const QByteArray &newValue)
{
    Q_ASSERT(!service.isNull());

    QJniEnvironment env;
    const jsize nativeSize = q26::saturate_cast<jsize>(newValue.size());
    jbyteArray payload;
    payload = env->NewByteArray(nativeSize);
    env->SetByteArrayRegion(payload, 0, nativeSize,
                            (jbyte *)newValue.constData());

    bool result = false;
    if (hub) {
        if (role == QLowEnergyController::CentralRole) {
            qCDebug(QT_BT_ANDROID) << "Write descriptor with handle " << descHandle
                     << newValue.toHex() << "(service:" << service->uuid << ")";
            result = hub->javaObject().callMethod<jboolean>("writeDescriptor",
                                                            descHandle, payload);
        } else {
            const auto &characteristic = characteristicForHandle(charHandle);
            const auto &descriptor = descriptorForHandle(descHandle);
            if (characteristic.isValid() && descriptor.isValid()) {
                qCDebug(QT_BT_ANDROID) << "Write descriptor" << descriptor.uuid()
                                   << "(service:" << service->uuid
                                   << "char: " << characteristic.uuid() << ")";
                const QJniObject charUuid = javaUuidfromQtUuid(characteristic.uuid());
                const QJniObject descUuid = javaUuidfromQtUuid(descriptor.uuid());
                result = hub->javaObject().callMethod<jboolean>(
                          "writeDescriptor",
                          service->androidService.object<QtJniTypes::BluetoothGattService>(),
                          charUuid.object<QtJniTypes::UUID>(), descUuid.object<QtJniTypes::UUID>(),
                          payload);
                if (result)
                    service->characteristicList[charHandle].descriptorList[descHandle].value = newValue;
            }
        }
    }

    env->DeleteLocalRef(payload);

    if (!result)
        service->setError(QLowEnergyService::DescriptorWriteError);
}

void QLowEnergyControllerPrivateAndroid::readCharacteristic(
        const QSharedPointer<QLowEnergyServicePrivate> service,
        const QLowEnergyHandle charHandle)
{
    Q_ASSERT(!service.isNull());

    if (!service->characteristicList.contains(charHandle))
        return;

    QJniEnvironment env;
    bool result = false;
    if (hub) {
        qCDebug(QT_BT_ANDROID) << "Read characteristic with handle"
                               <<  charHandle << service->uuid;
        result = hub->javaObject().callMethod<jboolean>("readCharacteristic", charHandle);
    }

    if (!result)
        service->setError(QLowEnergyService::CharacteristicReadError);
}

void QLowEnergyControllerPrivateAndroid::readDescriptor(
        const QSharedPointer<QLowEnergyServicePrivate> service,
        const QLowEnergyHandle /*charHandle*/,
        const QLowEnergyHandle descriptorHandle)
{
    Q_ASSERT(!service.isNull());

    QJniEnvironment env;
    bool result = false;
    if (hub) {
        qCDebug(QT_BT_ANDROID) << "Read descriptor with handle"
                               <<  descriptorHandle << service->uuid;
        result = hub->javaObject().callMethod<jboolean>("readDescriptor", descriptorHandle);
    }

    if (!result)
        service->setError(QLowEnergyService::DescriptorReadError);
}

void QLowEnergyControllerPrivateAndroid::connectionUpdated(
        QLowEnergyController::ControllerState newState,
        QLowEnergyController::Error errorCode)
{
    qCDebug(QT_BT_ANDROID) << "Connection updated:"
                           << "error:" << errorCode
                           << "oldState:" << state
                           << "newState:" << newState;

    if (role == QLowEnergyController::PeripheralRole)
        peripheralConnectionUpdated(newState, errorCode);
    else
        centralConnectionUpdated(newState, errorCode);
}

void QLowEnergyControllerPrivateAndroid::mtuChanged(int mtu)
{
    Q_Q(QLowEnergyController);
    qCDebug(QT_BT_ANDROID) << "MTU updated:"
                           << "mtu:" << mtu;
    emit q->mtuChanged(mtu);
}

void QLowEnergyControllerPrivateAndroid::remoteRssiRead(int rssi, bool success)
{
    Q_Q(QLowEnergyController);
    if (success) {
        // BT Core v5.3, 7.5.4, Vol 4, Part E
        // The LE RSSI can take values -127..127 => narrowing to qint16 is safe
        emit q->rssiRead(rssi);
    } else {
        qCDebug(QT_BT_ANDROID) << "Reading remote RSSI failed";
        setError(QLowEnergyController::RssiReadError);
    }
}

// called if server/peripheral
void QLowEnergyControllerPrivateAndroid::peripheralConnectionUpdated(
        QLowEnergyController::ControllerState newState,
        QLowEnergyController::Error errorCode)
{
    // Java errorCode can be larger than max QLowEnergyController::Error
    if (errorCode > QLowEnergyController::AdvertisingError)
        errorCode = QLowEnergyController::UnknownError;

    if (errorCode != QLowEnergyController::NoError)
        setError(errorCode);

    const QLowEnergyController::ControllerState oldState = state;
    setState(newState);

    // disconnect implies stop of advertisement
    if (newState == QLowEnergyController::UnconnectedState)
        stopAdvertising();

    // The remote name and address may have changed
    if (hub) {
        remoteDevice = QBluetoothAddress(
                    hub->javaObject().callMethod<jstring>("remoteAddress").toString());
        remoteName = hub->javaObject().callMethod<jstring>("remoteName").toString();
    }

    Q_Q(QLowEnergyController);
    // Emit (dis)connected if the connection state changes
    if (oldState == QLowEnergyController::ConnectedState
            && newState != QLowEnergyController::ConnectedState) {
        emit q->disconnected();
    } else if (newState == QLowEnergyController::ConnectedState
                 && oldState != QLowEnergyController::ConnectedState) {
        emit q->connected();
    }
}

// called if client/central
void QLowEnergyControllerPrivateAndroid::centralConnectionUpdated(
        QLowEnergyController::ControllerState newState,
        QLowEnergyController::Error errorCode)
{
    Q_Q(QLowEnergyController);

    const QLowEnergyController::ControllerState oldState = state;

    if (errorCode != QLowEnergyController::NoError) {
        // ConnectionError if transition from Connecting to Connected
        if (oldState == QLowEnergyController::ConnectingState) {
            setError(QLowEnergyController::ConnectionError);
            /* There is a bug in Android, when connecting to an unconnectable
             * device. The connection times out and Android sends error code
             * 133 (doesn't exist) and STATE_CONNECTED. A subsequent disconnect()
             * call never sends a STATE_DISCONNECTED either.
             * As workaround we will trigger disconnect when we encounter
             * error during connect attempt. This leaves the controller
             * in a cleaner state.
             * */
            newState = QLowEnergyController::UnconnectedState;
        }
        else
            setError(errorCode);
    }

    setState(newState);
    if (newState == QLowEnergyController::UnconnectedState
            && !(oldState == QLowEnergyController::UnconnectedState
                || oldState == QLowEnergyController::ConnectingState)) {

        // Invalidate the services if the disconnect came from the remote end.
        // Qtherwise we disconnected via QLowEnergyController::disconnectDevice() which
        // triggered invalidation already
        if (!serviceList.isEmpty()) {
            Q_ASSERT(oldState != QLowEnergyController::ClosingState);
            invalidateServices();
        }
        emit q->disconnected();
    } else if (newState == QLowEnergyController::ConnectedState
               && oldState != QLowEnergyController::ConnectedState ) {
        emit q->connected();
    }
}

void QLowEnergyControllerPrivateAndroid::servicesDiscovered(
        QLowEnergyController::Error errorCode, const QString &foundServices)
{
    Q_Q(QLowEnergyController);

    if (errorCode == QLowEnergyController::NoError) {
        //Android delivers all services in one go
        const QStringList list = foundServices.split(QChar::Space, Qt::SkipEmptyParts);
        for (const QString &entry : list) {
            const QBluetoothUuid service(entry);
            if (service.isNull())
                return;

            QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate();
            priv->uuid = service;
            priv->setController(this);

            QSharedPointer<QLowEnergyServicePrivate> pointer(priv);
            serviceList.insert(service, pointer);

            emit q->serviceDiscovered(QBluetoothUuid(entry));
        }

        setState(QLowEnergyController::DiscoveredState);
        emit q->discoveryFinished();
    } else {
        setError(errorCode);
        setState(QLowEnergyController::ConnectedState);
    }
}

void QLowEnergyControllerPrivateAndroid::serviceDetailsDiscoveryFinished(
        const QString &serviceUuid, int startHandle, int endHandle)
{
    const QBluetoothUuid service(serviceUuid);
    if (!serviceList.contains(service)) {
        qCWarning(QT_BT_ANDROID) << "Discovery done of unknown service:"
                                 << service.toString();
        return;
    }

    //update service data
    QSharedPointer<QLowEnergyServicePrivate> pointer =
            serviceList.value(service);
    pointer->startHandle = startHandle;
    pointer->endHandle = endHandle;

    if (hub && hub->javaObject().isValid()) {
        QJniObject uuid = QJniObject::fromString(serviceUuid);
        QJniObject javaIncludes = hub->javaObject().callMethod<jstring>(
                                        "includedServices", uuid.object<jstring>());
        if (javaIncludes.isValid()) {
            const QStringList list = javaIncludes.toString()
                                         .split(QChar::Space, Qt::SkipEmptyParts);
            for (const QString &entry : list) {
                const QBluetoothUuid service(entry);
                if (service.isNull())
                    return;

                pointer->includedServices.append(service);

                // update the type of the included service
                QSharedPointer<QLowEnergyServicePrivate> otherService =
                        serviceList.value(service);
                if (!otherService.isNull())
                    otherService->type |= QLowEnergyService::IncludedService;
            }
        }
    }

    qCDebug(QT_BT_ANDROID) << "Service" << serviceUuid << "discovered (start:"
              << startHandle << "end:" << endHandle << ")" << pointer.data();

    pointer->setState(QLowEnergyService::RemoteServiceDiscovered);
}

void QLowEnergyControllerPrivateAndroid::characteristicRead(
        const QBluetoothUuid &serviceUuid, int handle,
        const QBluetoothUuid &charUuid, int properties, const QByteArray &data)
{
    if (!serviceList.contains(serviceUuid))
        return;

    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceList.value(serviceUuid);
    QLowEnergyHandle charHandle = handle;

    QLowEnergyServicePrivate::CharData &charDetails =
            service->characteristicList[charHandle];

    //Android uses same property value as Qt which is the Bluetooth LE standard
    charDetails.properties = QLowEnergyCharacteristic::PropertyType(properties);
    charDetails.uuid = charUuid;
    charDetails.value = data;
    //value handle always one larger than characteristics value handle
    charDetails.valueHandle = charHandle + 1;

    if (service->state == QLowEnergyService::RemoteServiceDiscovered) {
        QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle);
        if (!characteristic.isValid()) {
            qCWarning(QT_BT_ANDROID) << "characteristicRead: Cannot find characteristic";
            return;
        }
        emit service->characteristicRead(characteristic, data);
    }
}

void QLowEnergyControllerPrivateAndroid::descriptorRead(
        const QBluetoothUuid &serviceUuid, const QBluetoothUuid &charUuid,
        int descHandle, const QBluetoothUuid &descUuid, const QByteArray &data)
{
    if (!serviceList.contains(serviceUuid))
        return;

    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceList.value(serviceUuid);

    bool entryUpdated = false;

    CharacteristicDataMap::iterator charIt = service->characteristicList.begin();
    for ( ; charIt != service->characteristicList.end(); ++charIt) {
        QLowEnergyServicePrivate::CharData &charDetails = charIt.value();

        if (charDetails.uuid != charUuid)
            continue;

        // new entry created if it doesn't exist
        QLowEnergyServicePrivate::DescData &descDetails =
                charDetails.descriptorList[descHandle];
        descDetails.uuid = descUuid;
        descDetails.value = data;
        entryUpdated = true;
        break;
    }

    if (!entryUpdated) {
        qCWarning(QT_BT_ANDROID) << "Cannot find/update descriptor"
                                 << descUuid << charUuid << serviceUuid;
    } else if (service->state == QLowEnergyService::RemoteServiceDiscovered){
        QLowEnergyDescriptor descriptor = descriptorForHandle(descHandle);
        if (!descriptor.isValid()) {
            qCWarning(QT_BT_ANDROID) << "descriptorRead: Cannot find descriptor";
            return;
        }
        emit service->descriptorRead(descriptor, data);
    }
}

void QLowEnergyControllerPrivateAndroid::characteristicWritten(
        int charHandle, const QByteArray &data, QLowEnergyService::ServiceError errorCode)
{
    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceForHandle(charHandle);
    if (service.isNull())
        return;

    qCDebug(QT_BT_ANDROID) << "Characteristic write confirmation" << service->uuid
                           << charHandle << data.toHex() << errorCode;

    if (errorCode != QLowEnergyService::NoError) {
        service->setError(errorCode);
        return;
    }

    QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle);
    if (!characteristic.isValid()) {
        qCWarning(QT_BT_ANDROID) << "characteristicWritten: Cannot find characteristic";
        return;
    }

    // only update cache when property is readable. Otherwise it remains
    // empty.
    if (characteristic.properties() & QLowEnergyCharacteristic::Read)
        updateValueOfCharacteristic(charHandle, data, false);
    emit service->characteristicWritten(characteristic, data);
}

void QLowEnergyControllerPrivateAndroid::descriptorWritten(
        int descHandle, const QByteArray &data, QLowEnergyService::ServiceError errorCode)
{
    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceForHandle(descHandle);
    if (service.isNull())
        return;

    qCDebug(QT_BT_ANDROID) << "Descriptor write confirmation" << service->uuid
                           << descHandle << data.toHex() << errorCode;

    if (errorCode != QLowEnergyService::NoError) {
        service->setError(errorCode);
        return;
    }

    QLowEnergyDescriptor descriptor = descriptorForHandle(descHandle);
    if (!descriptor.isValid()) {
        qCWarning(QT_BT_ANDROID) << "descriptorWritten: Cannot find descriptor";
        return;
    }

    updateValueOfDescriptor(descriptor.characteristicHandle(),
                            descHandle, data, false);
    emit service->descriptorWritten(descriptor, data);
}

void QLowEnergyControllerPrivateAndroid::serverDescriptorWritten(
        const QJniObject &jniDesc, const QByteArray &newValue)
{
    qCDebug(QT_BT_ANDROID) << "Server descriptor change notification" << newValue.toHex();

    // retrieve service, char and desc uuids
    const QJniObject jniChar = jniDesc.callMethod<QtJniTypes::BluetoothGattCharacteristic>(
                                            "getCharacteristic");
    if (!jniChar.isValid())
        return;

    const QJniObject jniService =
            jniChar.callMethod<QtJniTypes::BluetoothGattService>("getService");
    if (!jniService.isValid())
        return;

    QJniObject jniUuid = jniService.callMethod<QtJniTypes::UUID>("getUuid");
    const QBluetoothUuid serviceUuid(jniUuid.toString());
    if (serviceUuid.isNull())
        return;

    // TODO test if two service with same uuid exist
    if (!localServices.contains(serviceUuid))
        return;

    jniUuid = jniChar.callMethod<QtJniTypes::UUID>("getUuid");
    const QBluetoothUuid characteristicUuid(jniUuid.toString());
    if (characteristicUuid.isNull())
        return;

    jniUuid = jniDesc.callMethod<QtJniTypes::UUID>("getUuid");
    const QBluetoothUuid descriptorUuid(jniUuid.toString());
    if (descriptorUuid.isNull())
        return;

    // find matching QLEDescriptor
    auto servicePrivate = localServices.value(serviceUuid);
    // TODO test if service contains two characteristics with same uuid
    // or characteristic contains two descriptors with same uuid
    const auto handleList = servicePrivate->characteristicList.keys();
    for (const auto charHandle: handleList) {
        const auto &charData = servicePrivate->characteristicList.value(charHandle);
        if (charData.uuid != characteristicUuid)
            continue;

        const auto &descHandleList = charData.descriptorList.keys();
        for (const auto descHandle: descHandleList) {
            const auto &descData = charData.descriptorList.value(descHandle);
            if (descData.uuid != descriptorUuid)
                continue;

            qCDebug(QT_BT_ANDROID) << "serverDescriptorChanged: Matching descriptor"
                                   << descriptorUuid << "in char" << characteristicUuid
                                   << "of service" << serviceUuid;

            servicePrivate->characteristicList[charHandle].descriptorList[descHandle].value = newValue;

            emit servicePrivate->descriptorWritten(
                        QLowEnergyDescriptor(servicePrivate, charHandle, descHandle),
                        newValue);
            return;
        }
    }
}

void QLowEnergyControllerPrivateAndroid::characteristicChanged(
        int charHandle, const QByteArray &data)
{
    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceForHandle(charHandle);
    if (service.isNull())
        return;

    qCDebug(QT_BT_ANDROID) << "Characteristic change notification" << service->uuid
                           << charHandle << data.toHex() << "length:" << data.size();

    QLowEnergyCharacteristic characteristic = characteristicForHandle(charHandle);
    if (!characteristic.isValid()) {
        qCWarning(QT_BT_ANDROID) << "characteristicChanged: Cannot find characteristic";
        return;
    }

    // only update cache when property is readable. Otherwise it remains
    // empty.
    if (characteristic.properties() & QLowEnergyCharacteristic::Read)
        updateValueOfCharacteristic(characteristic.attributeHandle(),
                                data, false);
    emit service->characteristicChanged(characteristic, data);
}

void QLowEnergyControllerPrivateAndroid::serverCharacteristicChanged(
        const QJniObject &characteristic, const QByteArray &newValue)
{
    qCDebug(QT_BT_ANDROID) << "Server characteristic change notification"
        << newValue.toHex() << "length:" << newValue.size();

    // match characteristic to servicePrivate
    QJniObject service = characteristic.callMethod<QtJniTypes::BluetoothGattService>(
                                        "getService");
    if (!service.isValid())
        return;

    QJniObject jniUuid = service.callMethod<QtJniTypes::UUID>("getUuid");
    QBluetoothUuid serviceUuid(jniUuid.toString());
    if (serviceUuid.isNull())
        return;

    // TODO test if two service with same uuid exist
    if (!localServices.contains(serviceUuid))
        return;

    auto servicePrivate = localServices.value(serviceUuid);

    jniUuid = characteristic.callMethod<QtJniTypes::UUID>("getUuid");
    QBluetoothUuid characteristicUuid(jniUuid.toString());
    if (characteristicUuid.isNull())
        return;

    QLowEnergyHandle foundHandle = 0;
    const auto handleList = servicePrivate->characteristicList.keys();
    // TODO test if service contains two characteristics with same uuid
    for (const auto handle: handleList) {
        QLowEnergyServicePrivate::CharData &charData = servicePrivate->characteristicList[handle];
        if (charData.uuid != characteristicUuid)
            continue;

        qCDebug(QT_BT_ANDROID) << "serverCharacteristicChanged: Matching characteristic"
                               << characteristicUuid << " on " << serviceUuid;
        charData.value = newValue;
        foundHandle = handle;
        break;
    }

    if (!foundHandle)
        return;

    emit servicePrivate->characteristicChanged(
                QLowEnergyCharacteristic(servicePrivate, foundHandle), newValue);
}

void QLowEnergyControllerPrivateAndroid::serviceError(
        int attributeHandle, QLowEnergyService::ServiceError errorCode)
{
    // ignore call if it isn't really an error
    if (errorCode == QLowEnergyService::NoError)
        return;

    QSharedPointer<QLowEnergyServicePrivate> service =
            serviceForHandle(attributeHandle);
    Q_ASSERT(!service.isNull());

    // ATM we don't really use attributeHandle but later on we might
    // want to associate the error code with a char or desc
    service->setError(errorCode);
}

void QLowEnergyControllerPrivateAndroid::advertisementError(int errorCode)
{
    Q_Q(QLowEnergyController);

    switch (errorCode)
    {
    case 1: // AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE
        errorString = QLowEnergyController::tr("Advertisement data is larger than 31 bytes");
        break;
    case 2: // AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED
        errorString = QLowEnergyController::tr("Advertisement feature not supported on the platform");
        break;
    case 3: // AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR
        errorString = QLowEnergyController::tr("Error occurred trying to start advertising");
        break;
    case 4: // AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
        errorString = QLowEnergyController::tr("Failed due to too many advertisers");
        break;
    default:
        errorString = QLowEnergyController::tr("Unknown advertisement error");
        break;
    }

    error = QLowEnergyController::AdvertisingError;
    emit q->errorOccurred(error);

    // not relevant states in peripheral mode
    Q_ASSERT(state != QLowEnergyController::DiscoveredState);
    Q_ASSERT(state != QLowEnergyController::DiscoveringState);

    switch (state)
    {
    case QLowEnergyController::UnconnectedState:
    case QLowEnergyController::ConnectingState:
    case QLowEnergyController::ConnectedState:
    case QLowEnergyController::ClosingState:
        // noop as remote is already connected or about to disconnect.
        // when connection drops we reset to unconnected anyway
        break;

    case QLowEnergyController::AdvertisingState:
        setState(QLowEnergyController::UnconnectedState);
        break;
    default:
        break;
    }
}

static QJniObject javaParcelUuidfromQtUuid(const QBluetoothUuid &uuid)
{
    QString output = uuid.toString();
    // cut off leading and trailing brackets
    output = output.mid(1, output.size()-2);

    QJniObject javaString = QJniObject::fromString(output);
    QJniObject parcelUuid = QJniObject::callStaticMethod<QtJniTypes::ParcelUuid>(
                QtJniTypes::Traits<QtJniTypes::ParcelUuid>::className(), "fromString",
                javaString.object<jstring>());

    return parcelUuid;
}

static QJniObject createJavaAdvertiseData(const QLowEnergyAdvertisingData &data)
{
    QJniObject builder = QJniObject::construct<QtJniTypes::AdvertiseDataBuilder>();

    // device name cannot be set but there is choice to show it or not
    builder = builder.callMethod<QtJniTypes::AdvertiseDataBuilder>(
                                "setIncludeDeviceName", !data.localName().isEmpty());
    builder = builder.callMethod<QtJniTypes::AdvertiseDataBuilder>(
                                "setIncludeTxPowerLevel", data.includePowerLevel());
    const auto services = data.services();
    for (const auto &service : services) {
        builder = builder.callMethod<QtJniTypes::AdvertiseDataBuilder>("addServiceUuid",
                               javaParcelUuidfromQtUuid(service).object<QtJniTypes::ParcelUuid>());
    }

    if (!data.manufacturerData().isEmpty()) {
        QJniEnvironment env;
        const jsize nativeSize = q26::saturate_cast<jsize>(data.manufacturerData().size());
        jbyteArray nativeData = env->NewByteArray(nativeSize);
        env->SetByteArrayRegion(nativeData, 0, nativeSize,
                                reinterpret_cast<const jbyte*>(data.manufacturerData().constData()));
        builder = builder.callMethod<QtJniTypes::AdvertiseDataBuilder>(
                                    "addManufacturerData", jint(data.manufacturerId()), nativeData);
        env->DeleteLocalRef(nativeData);

        if (!builder.isValid()) {
            qCWarning(QT_BT_ANDROID) << "Cannot set manufacturer id/data";
        }
    }

    /*// TODO Qt vs Java API mismatch
          -> Qt assumes rawData() is a global field
          -> Android pairs rawData() per service uuid
    if (!data.rawData().isEmpty()) {
        QJniEnvironment env;
        const jsize nativeSize = q26::saturate_cast<jsize>(data.rawData().size());
        jbyteArray nativeData = env->NewByteArray(nativeSize);
        env->SetByteArrayRegion(nativeData, 0, nativeSize,
                                reinterpret_cast<const jbyte*>(data.rawData().constData()));
        builder = builder.callObjectMethod("addServiceData",
                                       "(Landroid/os/ParcelUuid;[B])Landroid/bluetooth/le/AdvertiseData$Builder;",
                                       data.rawData().object(), nativeData);
        env->DeleteLocalRef(nativeData);

        if (env.checkAndClearExceptions()) {
            qCWarning(QT_BT_ANDROID) << "Cannot set advertisement raw data";
        }
    }*/

    QJniObject javaAdvertiseData = builder.callMethod<QtJniTypes::AdvertiseData>("build");
    return javaAdvertiseData;
}

static QJniObject createJavaAdvertisingSetParameters(const QLowEnergyAdvertisingParameters &params,
                                                     const QJniObject &javaObj)
{
    QJniObject builder = QJniObject::construct<QtJniTypes::AdvertisingSetParametersBuilder>();

    // TODO: Extended advertising support (see QTBUG-144522).
    // We need a separate setting in the public API for it, because
    // old BTLE devices will not be able to understand it.
    // For now, always disable it!
#if 0
    const bool supportsExtendedAdvertising =
            javaObj.callMethod<jboolean>("isLeExtendedAdvertisingSupported");
    builder = builder.callMethod<QtJniTypes::AdvertisingSetParametersBuilder>(
                    "setLegacyMode", !supportsExtendedAdvertising);
#else
    Q_UNUSED(javaObj);
    builder = builder.callMethod<QtJniTypes::AdvertisingSetParametersBuilder>(
                    "setLegacyMode", true);
#endif

    bool connectable = false;
    bool scannable = false;
    switch (params.mode())
    {
    case QLowEnergyAdvertisingParameters::AdvInd:
        connectable = true;
        scannable = true; // TODO: if extended advertising is enabled, it must be false!
        break;
    case QLowEnergyAdvertisingParameters::AdvScanInd:
        connectable = false;
        scannable = true;
        break;
    case QLowEnergyAdvertisingParameters::AdvNonConnInd:
        connectable = false;
        scannable = false;
        break;
    // intentionally no default case
    }
    builder = builder.callMethod<QtJniTypes::AdvertisingSetParametersBuilder>(
                    "setConnectable", connectable);
    builder = builder.callMethod<QtJniTypes::AdvertisingSetParametersBuilder>(
                    "setScannable", scannable);

    // The QLowEnergyAdvertisingParameters struct contains the min and max
    // interval values in milliseconds. However, the Android docs state
    // the following:
    // Bluetooth LE Advertising interval, in 0.625ms unit.
    // Valid range is from 160 (100ms) to 16777215 (10,485.759375 s).
    auto advertisingSetParametersObj =
            QJniEnvironment().findClass<QtJniTypes::AdvertisingSetParameters>();
    static const int minAdvInterval =
            QJniObject::getStaticField<jint>(advertisingSetParametersObj, "INTERVAL_MIN");
    static const int maxAdvInterval =
            QJniObject::getStaticField<jint>(advertisingSetParametersObj, "INTERVAL_MAX");

    int desiredInterval = (params.minimumInterval() + params.maximumInterval()) / 2 / 0.625;
    if (desiredInterval < minAdvInterval)
        desiredInterval = minAdvInterval;
    else if (desiredInterval > maxAdvInterval)
        desiredInterval = maxAdvInterval;

    builder = builder.callMethod<QtJniTypes::AdvertisingSetParametersBuilder>(
                    "setInterval", desiredInterval);

    QJniObject javaAdvertisingSetParameters =
            builder.callMethod<QtJniTypes::AdvertisingSetParameters>("build");
    return javaAdvertisingSetParameters;
}


void QLowEnergyControllerPrivateAndroid::startAdvertising(const QLowEnergyAdvertisingParameters &params,
        const QLowEnergyAdvertisingData &advertisingData,
        const QLowEnergyAdvertisingData &scanResponseData)
{
    setState(QLowEnergyController::AdvertisingState);

    if (!ensureAndroidPermission(QBluetoothPermission::Access | QBluetoothPermission::Advertise)) {
        qCWarning(QT_BT_ANDROID) << "startAdvertising() failed due to missing permissions";
        setError(QLowEnergyController::MissingPermissionsError);
        setState(QLowEnergyController::UnconnectedState);
        return;
    }

    if (!hub || !hub->javaObject().isValid()) {
        qCWarning(QT_BT_ANDROID) << "Cannot initiate QtBluetoothLEServer";
        setError(QLowEnergyController::AdvertisingError);
        setState(QLowEnergyController::UnconnectedState);
        return;
    }

    // Pass on advertisingData, scanResponse & AdvertisingSetParameters
    QJniObject jAdvertiseData = createJavaAdvertiseData(advertisingData);
    QJniObject jScanResponse = createJavaAdvertiseData(scanResponseData);
    QJniObject jAdvertisingSetParameters =
                    createJavaAdvertisingSetParameters(params, hub->javaObject());

    const bool result = hub->javaObject().callMethod<jboolean>("startAdvertising",
                        jAdvertiseData.object<QtJniTypes::AdvertiseData>(),
                        jScanResponse.object<QtJniTypes::AdvertiseData>(),
                        jAdvertisingSetParameters.object<QtJniTypes::AdvertisingSetParameters>());
    if (!result) {
        setError(QLowEnergyController::AdvertisingError);
        setState(QLowEnergyController::UnconnectedState);
    }
}

void QLowEnergyControllerPrivateAndroid::stopAdvertising()
{
    setState(QLowEnergyController::UnconnectedState);
    hub->javaObject().callMethod<void>("stopAdvertising");
}

void QLowEnergyControllerPrivateAndroid::requestConnectionUpdate(const QLowEnergyConnectionParameters &params)
{
    // Android does not permit specification of specific latency or min/max
    // connection intervals (see BluetoothGatt.requestConnectionPriority()
    // In fact, each device manufacturer is permitted to change those values via a config
    // file too. Therefore we can only make an approximated guess (see implementation below)
    // In addition there is no feedback signal (known bug) from the hardware layer as per v24.

    // TODO recheck in later Android releases whether callback for
    // BluetoothGatt.requestConnectionPriority() was added

    if (role != QLowEnergyController::CentralRole) {
        qCWarning(QT_BT_ANDROID) << "On Android, connection requests only work for central role";
        return;
    }

    const bool result = hub->javaObject().callMethod<jboolean>("requestConnectionUpdatePriority",
                                                                params.minimumInterval());
    if (!result)
        qCWarning(QT_BT_ANDROID) << "Cannot set connection update priority";
}

/*
 * Returns the Java char permissions based on the given characteristic data.
 */
static int setupCharPermissions(const QLowEnergyCharacteristicData &charData)
{
    int permission = 0;
    if (charData.properties() & QLowEnergyCharacteristic::Read) {
        if (int(charData.readConstraints()) == 0 // nothing is equivalent to simple read
            || (charData.readConstraints()
                & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_READ");
        }

        if (charData.readConstraints()
            & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_READ_ENCRYPTED");
        }

        if (charData.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_READ_ENCRYPTED_MITM");
        }
    }

    if (charData.properties() &
                (QLowEnergyCharacteristic::Write|QLowEnergyCharacteristic::WriteNoResponse) ) {
        if (int(charData.writeConstraints()) == 0 // no flag is equivalent ti simple write
            || (charData.writeConstraints()
                & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_WRITE");
        }

        if (charData.writeConstraints()
            & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_WRITE_ENCRYPTED");
        }

        if (charData.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_WRITE_ENCRYPTED_MITM");
        }
    }

    if (charData.properties() & QLowEnergyCharacteristic::WriteSigned) {
        if (charData.writeConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_WRITE_SIGNED_MITM");
        } else {
            permission |= QJniObject::getStaticField<jint>(
                           QtJniTypes::Traits<QtJniTypes::BluetoothGattCharacteristic>::className(),
                           "PERMISSION_WRITE_SIGNED");
        }
    }

    return permission;
}

/*
 * Returns the Java desc permissions based on the given descriptor data.
 */
static int setupDescPermissions(const QLowEnergyDescriptorData &descData)
{
    int permissions = 0;

    if (descData.isReadable()) {
        if (int(descData.readConstraints()) == 0 // empty is equivalent to simple read
            || (descData.readConstraints()
                & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_READ");
        }

        if (descData.readConstraints()
            & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_READ_ENCRYPTED");
        }

        if (descData.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_READ_ENCRYPTED_MITM");
        }
    }

    if (descData.isWritable()) {
        if (int(descData.readConstraints()) == 0 // empty is equivalent to simple read
            || (descData.readConstraints()
                & QBluetooth::AttAccessConstraint::AttAuthorizationRequired)) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_WRITE");
        }

        if (descData.readConstraints()
            & QBluetooth::AttAccessConstraint::AttAuthenticationRequired) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_WRITE_ENCRYPTED");
        }

        if (descData.readConstraints() & QBluetooth::AttAccessConstraint::AttEncryptionRequired) {
            permissions |= QJniObject::getStaticField<jint>(
                               QtJniTypes::Traits<QtJniTypes::BluetoothGattDescriptor>::className(),
                               "PERMISSION_WRITE_ENCRYPTED_MITM");
        }
    }

    return permissions;
}

void QLowEnergyControllerPrivateAndroid::addToGenericAttributeList(const QLowEnergyServiceData &serviceData,
                                                            QLowEnergyHandle startHandle)
{
    QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(startHandle);
    if (service.isNull())
        return;

    // create BluetoothGattService object
    jint sType = QJniObject::getStaticField<jint>(
                            QtJniTypes::Traits<QtJniTypes::BluetoothGattService>::className(),
                            "SERVICE_TYPE_PRIMARY");
    if (serviceData.type() == QLowEnergyServiceData::ServiceTypeSecondary)
        sType = QJniObject::getStaticField<jint>(
                            QtJniTypes::Traits<QtJniTypes::BluetoothGattService>::className(),
                            "SERVICE_TYPE_SECONDARY");

    service->androidService = QJniObject::construct<QtJniTypes::BluetoothGattService>(
                javaUuidfromQtUuid(service->uuid).object<QtJniTypes::UUID>(), sType);

    // add included services, which must have been added earlier already
    const QList<QLowEnergyService*> includedServices = serviceData.includedServices();
    for (const auto includedServiceEntry: includedServices) {
        //TODO test this end-to-end
        const jboolean result = service->androidService.callMethod<jboolean>("addService",
                        includedServiceEntry->d_ptr->androidService.object<QtJniTypes::BluetoothGattService>());
        if (!result)
            qWarning(QT_BT_ANDROID) << "Cannot add included service " << includedServiceEntry->serviceUuid()
                                    << "to current service" << service->uuid;
    }

    // add characteristics
    const QList<QLowEnergyCharacteristicData> serviceCharsData = serviceData.characteristics();
    for (const auto &charData: serviceCharsData) {

        // User may have set minimum and/or maximum characteristic value size. Enforce
        // them here. If the user has not defined these limits, the default values 0..INT_MAX
        // do not limit anything. If the sizes are violated here (ie. when adding the
        // characteristics), regard it as a programming error.
        if (charData.value().size() < charData.minimumValueLength() ||
            charData.value().size() > charData.maximumValueLength()) {
            qWarning() << "Warning: Ignoring characteristic" << charData.uuid()
                       << "with invalid length:"  << charData.value().size()
                       << "(minimum:" << charData.minimumValueLength()
                       << "maximum:" << charData.maximumValueLength() << ").";
            continue;
        }

        // Issue a warning if the attribute length exceeds the bluetooth standard limit.
        if (charData.value().size() > BTLE_MAX_ATTRIBUTE_VALUE_SIZE) {
            qCWarning(QT_BT_ANDROID) << "Warning: characteristic" << charData.uuid() << "size"
                                     << "exceeds the standard: " << BTLE_MAX_ATTRIBUTE_VALUE_SIZE
                                     << ", value size:" << charData.value().size();
        }

        QJniObject javaChar = QJniObject::construct<QtJniTypes::QtBtGattCharacteristic>(
                           javaUuidfromQtUuid(charData.uuid()).object<QtJniTypes::UUID>(),
                           int(charData.properties()),
                           setupCharPermissions(charData),
                           charData.minimumValueLength(),
                           charData.maximumValueLength());

        QJniEnvironment env;
        const jsize nativeCharSize = q26::saturate_cast<jsize>(charData.value().size());
        jbyteArray jb = env->NewByteArray(nativeCharSize);
        env->SetByteArrayRegion(jb, 0, nativeCharSize, (jbyte*)charData.value().data());
        jboolean success = javaChar.callMethod<jboolean>("setLocalValue", jb);
        if (!success)
            qCWarning(QT_BT_ANDROID) << "Cannot setup initial characteristic value for " << charData.uuid();
        env->DeleteLocalRef(jb);

        const QList<QLowEnergyDescriptorData> descriptorList = charData.descriptors();
        for (const auto &descData: descriptorList) {
            QJniObject javaDesc = QJniObject::construct<QtJniTypes::QtBtGattDescriptor>(
                                    javaUuidfromQtUuid(descData.uuid()).object<QtJniTypes::UUID>(),
                                    setupDescPermissions(descData));

            const jsize nativeDescSize = q26::saturate_cast<jsize>(descData.value().size());
            jb = env->NewByteArray(nativeDescSize);
            env->SetByteArrayRegion(jb, 0, nativeDescSize, (jbyte*)descData.value().data());
            success = javaDesc.callMethod<jboolean>("setLocalValue", jb);
            if (!success) {
                qCWarning(QT_BT_ANDROID) << "Cannot setup initial descriptor value for "
                                         << descData.uuid() << "(char" << charData.uuid()
                                         << "on service " << service->uuid << ")";
            }

            env->DeleteLocalRef(jb);


            success = javaChar.callMethod<jboolean>("addDescriptor",
                                          javaDesc.object<QtJniTypes::BluetoothGattDescriptor>());
            if (!success) {
                qCWarning(QT_BT_ANDROID) << "Cannot add descriptor" << descData.uuid()
                                         << "to service" << service->uuid << "(char:"
                                         << charData.uuid() << ")";
            }
        }

        success = service->androidService.callMethod<jboolean>(
                                    "addCharacteristic",
                                    javaChar.object<QtJniTypes::BluetoothGattCharacteristic>());
        if (!success) {
            qCWarning(QT_BT_ANDROID) << "Cannot add characteristic" << charData.uuid()
                                     << "to service" << service->uuid;
        }
    }

    hub->javaObject().callMethod<void>("addService",
                            service->androidService.object<QtJniTypes::BluetoothGattService>());
}

int QLowEnergyControllerPrivateAndroid::mtu() const
{
    if (!hub) {
        qCWarning(QT_BT_ANDROID) << "could not determine MTU, hub is does not exist";
        return -1;
    } else {
        int result = hub->javaObject().callMethod<int>("mtu");
        qCDebug(QT_BT_ANDROID) << "MTU found to be" << result;
        return result;
    }
}

void QLowEnergyControllerPrivateAndroid::readRssi()
{
    if (!hub || !hub->javaObject().callMethod<jboolean>("readRemoteRssi")) {
        qCWarning(QT_BT_ANDROID) << "request to read RSSI failed";
        setError(QLowEnergyController::RssiReadError);
        return;
    }
}

QT_END_NAMESPACE
