diff options
Diffstat (limited to 'src/bluetooth/qlowenergycontroller_android.cpp')
-rw-r--r-- | src/bluetooth/qlowenergycontroller_android.cpp | 767 |
1 files changed, 723 insertions, 44 deletions
diff --git a/src/bluetooth/qlowenergycontroller_android.cpp b/src/bluetooth/qlowenergycontroller_android.cpp index 767c91f8..a1decd96 100644 --- a/src/bluetooth/qlowenergycontroller_android.cpp +++ b/src/bluetooth/qlowenergycontroller_android.cpp @@ -1,31 +1,37 @@ /**************************************************************************** ** -** Copyright (C) 2015 The Qt Company Ltd. -** Contact: http://www.qt.io/licensing/ +** 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:LGPL21$ +** $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 http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. +** 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 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** 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. ** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** 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$ ** @@ -34,11 +40,35 @@ #include "qlowenergycontroller_p.h" #include <QtCore/QLoggingCategory> #include <QtAndroidExtras/QAndroidJniEnvironment> +#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) +Q_DECLARE_METATYPE(QAndroidJniObject) + +// Conversion: QBluetoothUuid -> java.util.UUID +static QAndroidJniObject javaUuidfromQtUuid(const QBluetoothUuid& uuid) +{ + QString output = uuid.toString(); + // cut off leading and trailing brackets + output = output.mid(1, output.size()-2); + + QAndroidJniObject javaString = QAndroidJniObject::fromString(output); + QAndroidJniObject javaUuid = QAndroidJniObject::callStaticObjectMethod( + "java/util/UUID", "fromString", "(Ljava/lang/String;)Ljava/util/UUID;", + javaString.object()); + + return javaUuid; +} + QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() : QObject(), state(QLowEnergyController::UnconnectedState), @@ -50,21 +80,48 @@ QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() QLowEnergyControllerPrivate::~QLowEnergyControllerPrivate() { + if (role == QLowEnergyController::PeripheralRole) { + if (hub) + hub->javaObject().callMethod<void>("disconnectServer"); + } } -void QLowEnergyControllerPrivate::connectToDevice() +void QLowEnergyControllerPrivate::init() { - // required to pass unit test on default backend - if (remoteDevice.isNull()) { - qWarning() << "Invalid/null remote device address"; - setError(QLowEnergyController::UnknownRemoteDeviceError); - return; - } + // Android Central/Client support starts with v18 + // Peripheral/Server support requires Android API v21 + const bool isPeripheral = (role == QLowEnergyController::PeripheralRole); + const jint version = QtAndroidPrivate::androidSdkVersion(); + + if (isPeripheral) { + if (version < 21) { + qWarning() << "Qt Bluetooth LE Peripheral support not available" + "on Android devices below version 21"; + return; + } - setState(QLowEnergyController::ConnectingState); + qRegisterMetaType<QAndroidJniObject>(); + + 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, &QLowEnergyControllerPrivate::connectionUpdated); + connect(hub, &LowEnergyNotificationHub::advertisementError, + this, &QLowEnergyControllerPrivate::advertisementError); + connect(hub, &LowEnergyNotificationHub::serverCharacteristicChanged, + this, &QLowEnergyControllerPrivate::serverCharacteristicChanged); + connect(hub, &LowEnergyNotificationHub::serverDescriptorWritten, + this, &QLowEnergyControllerPrivate::serverDescriptorWritten); + } else { + if (version < 18) { + qWarning() << "Qt Bluetooth LE Central/Client support not available" + "on Android devices below version 18"; + return; + } - if (!hub) { - hub = new LowEnergyNotificationHub(remoteDevice, this); + hub = new LowEnergyNotificationHub(remoteDevice, isPeripheral, this); + // we only connect to the central role specific signals connect(hub, &LowEnergyNotificationHub::connectionUpdated, this, &QLowEnergyControllerPrivate::connectionUpdated); connect(hub, &LowEnergyNotificationHub::servicesDiscovered, @@ -81,9 +138,22 @@ void QLowEnergyControllerPrivate::connectToDevice() this, &QLowEnergyControllerPrivate::descriptorWritten); connect(hub, &LowEnergyNotificationHub::characteristicChanged, this, &QLowEnergyControllerPrivate::characteristicChanged); - connect(hub, &LowEnergyNotificationHub::serviceError, - this, &QLowEnergyControllerPrivate::serviceError); } +} + +void QLowEnergyControllerPrivate::connectToDevice() +{ + if (!hub) + return; // Android version below v18 + + // required to pass unit test on default backend + if (remoteDevice.isNull()) { + qWarning() << "Invalid/null remote device address"; + setError(QLowEnergyController::UnknownRemoteDeviceError); + return; + } + + setState(QLowEnergyController::ConnectingState); if (!hub->javaObject().isValid()) { qCWarning(QT_BT_ANDROID) << "Cannot initiate QtBluetoothLE"; @@ -168,7 +238,7 @@ void QLowEnergyControllerPrivate::writeCharacteristic( const QSharedPointer<QLowEnergyServicePrivate> service, const QLowEnergyHandle charHandle, const QByteArray &newValue, - bool writeWithResponse) + QLowEnergyService::WriteMode mode) { //TODO don't ignore WriteWithResponse, right now we assume responses Q_ASSERT(!service.isNull()); @@ -184,12 +254,26 @@ void QLowEnergyControllerPrivate::writeCharacteristic( bool result = false; if (hub) { - qCDebug(QT_BT_ANDROID) << "Write characteristic with handle " << charHandle - << newValue.toHex() << "(service:" << service->uuid - << ", writeWithResponse:" << writeWithResponse << ")"; - result = hub->javaObject().callMethod<jboolean>("writeCharacteristic", "(I[BI)Z", - charHandle, payload, - writeWithResponse ? QLowEnergyService::WriteWithResponse : QLowEnergyService::WriteWithoutResponse); + 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", "(I[BI)Z", + charHandle, payload, 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 QAndroidJniObject charUuid = javaUuidfromQtUuid(characteristic.uuid()); + result = hub->javaObject().callMethod<jboolean>( + "writeCharacteristic", + "(Landroid/bluetooth/BluetoothGattService;Ljava/util/UUID;[B)Z", + service->androidService.object(), charUuid.object(), payload); + } + } } if (env->ExceptionOccurred()) { @@ -206,7 +290,7 @@ void QLowEnergyControllerPrivate::writeCharacteristic( void QLowEnergyControllerPrivate::writeDescriptor( const QSharedPointer<QLowEnergyServicePrivate> service, - const QLowEnergyHandle /*charHandle*/, + const QLowEnergyHandle charHandle, const QLowEnergyHandle descHandle, const QByteArray &newValue) { @@ -220,10 +304,27 @@ void QLowEnergyControllerPrivate::writeDescriptor( bool result = false; if (hub) { - qCDebug(QT_BT_ANDROID) << "Write descriptor with handle " << descHandle - << newValue.toHex() << "(service:" << service->uuid << ")"; - result = hub->javaObject().callMethod<jboolean>("writeDescriptor", "(I[B)Z", - descHandle, payload); + if (role == QLowEnergyController::CentralRole) { + qCDebug(QT_BT_ANDROID) << "Write descriptor with handle " << descHandle + << newValue.toHex() << "(service:" << service->uuid << ")"; + result = hub->javaObject().callMethod<jboolean>("writeDescriptor", "(I[B)Z", + 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 QAndroidJniObject charUuid = javaUuidfromQtUuid(characteristic.uuid()); + const QAndroidJniObject descUuid = javaUuidfromQtUuid(descriptor.uuid()); + result = hub->javaObject().callMethod<jboolean>( + "writeDescriptor", + "(Landroid/bluetooth/BluetoothGattService;Ljava/util/UUID;Ljava/util/UUID;[B)Z", + service->androidService.object(), charUuid.object(), + descUuid.object(), payload); + } + } } if (env->ExceptionOccurred()) { @@ -263,7 +364,7 @@ void QLowEnergyControllerPrivate::readCharacteristic( } if (!result) - service->setError(QLowEnergyService::CharacteristicWriteError); + service->setError(QLowEnergyService::CharacteristicReadError); } void QLowEnergyControllerPrivate::readDescriptor( @@ -289,21 +390,63 @@ void QLowEnergyControllerPrivate::readDescriptor( } if (!result) - service->setError(QLowEnergyService::DescriptorWriteError); + service->setError(QLowEnergyService::DescriptorReadError); } void QLowEnergyControllerPrivate::connectionUpdated( QLowEnergyController::ControllerState newState, QLowEnergyController::Error errorCode) { - Q_Q(QLowEnergyController); - - const QLowEnergyController::ControllerState oldState = state; qCDebug(QT_BT_ANDROID) << "Connection updated:" << "error:" << errorCode - << "oldState:" << oldState + << "oldState:" << state << "newState:" << newState; + if (role == QLowEnergyController::PeripheralRole) + peripheralConnectionUpdated(newState, errorCode); + else + centralConnectionUpdated(newState, errorCode); +} + +// called if server/peripheral +void QLowEnergyControllerPrivate::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(); + + + Q_Q(QLowEnergyController); + 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 QLowEnergyControllerPrivate::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) { @@ -326,6 +469,14 @@ void QLowEnergyControllerPrivate::connectionUpdated( 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 ) { @@ -540,6 +691,71 @@ void QLowEnergyControllerPrivate::descriptorWritten( emit service->descriptorWritten(descriptor, data); } +void QLowEnergyControllerPrivate::serverDescriptorWritten( + const QAndroidJniObject &jniDesc, const QByteArray &newValue) +{ + qCDebug(QT_BT_ANDROID) << "Server descriptor change notification" << newValue.toHex(); + + // retrieve service, char and desc uuids + QAndroidJniObject jniChar = jniDesc.callObjectMethod( + "getCharacteristic", "()Landroid/bluetooth/BluetoothGattCharacteristic;"); + if (!jniChar.isValid()) + return; + + QAndroidJniObject jniService = jniChar.callObjectMethod( + "getService", "()Landroid/bluetooth/BluetoothGattService;"); + if (!jniService.isValid()) + return; + + QAndroidJniObject jniUuid = jniService.callObjectMethod("getUuid", "()Ljava/util/UUID;"); + 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.callObjectMethod("getUuid", "()Ljava/util/UUID;"); + const QBluetoothUuid characteristicUuid(jniUuid.toString()); + if (characteristicUuid.isNull()) + return; + + jniUuid = jniDesc.callObjectMethod("getUuid", "()Ljava/util/UUID;"); + 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 QLowEnergyControllerPrivate::characteristicChanged( int charHandle, const QByteArray &data) { @@ -565,6 +781,55 @@ void QLowEnergyControllerPrivate::characteristicChanged( emit service->characteristicChanged(characteristic, data); } +void QLowEnergyControllerPrivate::serverCharacteristicChanged( + const QAndroidJniObject &characteristic, const QByteArray &newValue) +{ + qCDebug(QT_BT_ANDROID) << "Server characteristic change notification" << newValue.toHex(); + + // match characteristic to servicePrivate + QAndroidJniObject service = characteristic.callObjectMethod( + "getService", "()Landroid/bluetooth/BluetoothGattService;"); + if (!service.isValid()) + return; + + QAndroidJniObject jniUuid = service.callObjectMethod("getUuid", "()Ljava/util/UUID;"); + 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.callObjectMethod("getUuid", "()Ljava/util/UUID;"); + 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 QLowEnergyControllerPrivate::serviceError( int attributeHandle, QLowEnergyService::ServiceError errorCode) { @@ -581,4 +846,418 @@ void QLowEnergyControllerPrivate::serviceError( service->setError(errorCode); } +void QLowEnergyControllerPrivate::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->error(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 QAndroidJniObject javaParcelUuidfromQtUuid(const QBluetoothUuid &uuid) +{ + QString output = uuid.toString(); + // cut off leading and trailing brackets + output = output.mid(1, output.size()-2); + + QAndroidJniObject javaString = QAndroidJniObject::fromString(output); + QAndroidJniObject parcelUuid = QAndroidJniObject::callStaticObjectMethod( + "android/os/ParcelUuid", "fromString", + "(Ljava/lang/String;)Landroid/os/ParcelUuid;", javaString.object()); + + return parcelUuid; +} + +static QAndroidJniObject createJavaAdvertiseData(const QLowEnergyAdvertisingData &data) +{ + QAndroidJniObject builder = QAndroidJniObject("android/bluetooth/le/AdvertiseData$Builder"); + + // device name cannot be set but there is choice to show it or not + builder = builder.callObjectMethod("setIncludeDeviceName", "(Z)Landroid/bluetooth/le/AdvertiseData$Builder;", + !data.localName().isEmpty()); + builder = builder.callObjectMethod("setIncludeTxPowerLevel", "(Z)Landroid/bluetooth/le/AdvertiseData$Builder;", + data.includePowerLevel()); + for (const auto service: data.services()) + { + builder = builder.callObjectMethod("addServiceUuid", + "(Landroid/os/ParcelUuid;)Landroid/bluetooth/le/AdvertiseData$Builder;", + javaParcelUuidfromQtUuid(service).object()); + } + + if (!data.manufacturerData().isEmpty()) { + QAndroidJniEnvironment env; + const qint32 nativeSize = data.manufacturerData().size(); + jbyteArray nativeData = env->NewByteArray(nativeSize); + env->SetByteArrayRegion(nativeData, 0, nativeSize, + reinterpret_cast<const jbyte*>(data.manufacturerData().constData())); + builder = builder.callObjectMethod("addManufacturerData", + "(I[B])Landroid/bluetooth/le/AdvertiseData$Builder;", + data.manufacturerId(), nativeData); + env->DeleteLocalRef(nativeData); + + if (env->ExceptionCheck()) { + qCWarning(QT_BT_ANDROID) << "Cannot set manufacturer id/data"; + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } + + /*// TODO Qt vs Java API mismatch + -> Qt assumes rawData() is a global field + -> Android pairs rawData() per service uuid + if (!data.rawData().isEmpty()) { + QAndroidJniEnvironment env; + qint32 nativeSize = 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->ExceptionCheck()) { + qCWarning(QT_BT_ANDROID) << "Cannot set advertisement raw data"; + env->ExceptionDescribe(); + env->ExceptionClear(); + } + }*/ + + QAndroidJniObject javaAdvertiseData = builder.callObjectMethod("build", + "()Landroid/bluetooth/le/AdvertiseData;"); + return javaAdvertiseData; +} + +static QAndroidJniObject createJavaAdvertiseSettings(const QLowEnergyAdvertisingParameters ¶ms) +{ + QAndroidJniObject builder = QAndroidJniObject("android/bluetooth/le/AdvertiseSettings$Builder"); + + bool connectable = false; + switch (params.mode()) + { + case QLowEnergyAdvertisingParameters::AdvInd: + connectable = true; + break; + case QLowEnergyAdvertisingParameters::AdvScanInd: + case QLowEnergyAdvertisingParameters::AdvNonConnInd: + connectable = false; + break; + // intentionally no default case + } + builder = builder.callObjectMethod("setConnectable", "(Z)Landroid/bluetooth/le/AdvertiseSettings$Builder;", + connectable); + + /* TODO No Android API for further QLowEnergyAdvertisingParameters options + * Android TxPowerLevel, AdvertiseMode and Timeout not mappable to Qt + */ + + QAndroidJniObject javaAdvertiseSettings = builder.callObjectMethod("build", + "()Landroid/bluetooth/le/AdvertiseSettings;"); + return javaAdvertiseSettings; +} + + +void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, + const QLowEnergyAdvertisingData &advertisingData, + const QLowEnergyAdvertisingData &scanResponseData) +{ + setState(QLowEnergyController::AdvertisingState); + + if (!hub->javaObject().isValid()) { + qCWarning(QT_BT_ANDROID) << "Cannot initiate QtBluetoothLEServer"; + setError(QLowEnergyController::AdvertisingError); + setState(QLowEnergyController::UnconnectedState); + return; + } + + // Pass on advertisingData, scanResponse & AdvertiseSettings + QAndroidJniObject jAdvertiseData = createJavaAdvertiseData(advertisingData); + QAndroidJniObject jScanResponse = createJavaAdvertiseData(scanResponseData); + QAndroidJniObject jAdvertiseSettings = createJavaAdvertiseSettings(params); + + const bool result = hub->javaObject().callMethod<jboolean>("startAdvertising", + "(Landroid/bluetooth/le/AdvertiseData;Landroid/bluetooth/le/AdvertiseData;Landroid/bluetooth/le/AdvertiseSettings;)Z", + jAdvertiseData.object(), jScanResponse.object(), jAdvertiseSettings.object()); + if (!result) { + setError(QLowEnergyController::AdvertisingError); + setState(QLowEnergyController::UnconnectedState); + } +} + +void QLowEnergyControllerPrivate::stopAdvertising() +{ + setState(QLowEnergyController::UnconnectedState); + hub->javaObject().callMethod<void>("stopAdvertising"); +} + +void QLowEnergyControllerPrivate::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) +{ + // Possible since Android v21 + // 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", + "(D)Z", 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::AttAuthorizationRequired)) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_READ"); + } + + if (charData.readConstraints() & QBluetooth::AttAuthenticationRequired) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_READ_ENCRYPTED"); + } + + if (charData.readConstraints() & QBluetooth::AttEncryptionRequired) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "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::AttAuthorizationRequired)) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_WRITE"); + } + + if (charData.writeConstraints() & QBluetooth::AttAuthenticationRequired) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_WRITE_ENCRYPTED"); + } + + if (charData.writeConstraints() & QBluetooth::AttEncryptionRequired) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_WRITE_ENCRYPTED_MITM"); + } + } + + if (charData.properties() & QLowEnergyCharacteristic::WriteSigned) { + if (charData.writeConstraints() & QBluetooth::AttEncryptionRequired) { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "PERMISSION_WRITE_SIGNED_MITM"); + } else { + permission |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattCharacteristic", + "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::AttAuthorizationRequired)) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_READ"); + } + + if (descData.readConstraints() & QBluetooth::AttAuthenticationRequired) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_READ_ENCRYPTED"); + } + + if (descData.readConstraints() & QBluetooth::AttEncryptionRequired) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_READ_ENCRYPTED_MITM"); + } + } + + if (descData.isWritable()) { + if (int(descData.readConstraints()) == 0 // empty is equivalent to simple read + || (descData.readConstraints() & QBluetooth::AttAuthorizationRequired)) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_WRITE"); + } + + if (descData.readConstraints() & QBluetooth::AttAuthenticationRequired) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_WRITE_ENCRYPTED"); + } + + if (descData.readConstraints() & QBluetooth::AttEncryptionRequired) { + permissions |= QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattDescriptor", + "PERMISSION_WRITE_ENCRYPTED_MITM"); + } + } + + return permissions; +} + +void QLowEnergyControllerPrivate::addToGenericAttributeList(const QLowEnergyServiceData &serviceData, + QLowEnergyHandle startHandle) +{ + QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(startHandle); + if (service.isNull()) + return; + + // create BluetoothGattService object + jint sType = QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattService", "SERVICE_TYPE_PRIMARY"); + if (serviceData.type() == QLowEnergyServiceData::ServiceTypeSecondary) + sType = QAndroidJniObject::getStaticField<jint>( + "android/bluetooth/BluetoothGattService", "SERVICE_TYPE_SECONDARY"); + + service->androidService = QAndroidJniObject("android/bluetooth/BluetoothGattService", + "(Ljava/util/UUID;I)V", + javaUuidfromQtUuid(service->uuid).object(), 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", "(Landroid/bluetooth/BluetoothGattService;)Z", + includedServiceEntry->d_ptr->androidService.object()); + 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) { + QAndroidJniObject javaChar = QAndroidJniObject("android/bluetooth/BluetoothGattCharacteristic", + "(Ljava/util/UUID;II)V", + javaUuidfromQtUuid(charData.uuid()).object(), + int(charData.properties()), + setupCharPermissions(charData)); + + QAndroidJniEnvironment env; + jbyteArray jb = env->NewByteArray(charData.value().size()); + env->SetByteArrayRegion(jb, 0, charData.value().size(), (jbyte*)charData.value().data()); + jboolean success = javaChar.callMethod<jboolean>("setValue", "([B)Z", 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) { + QAndroidJniObject javaDesc = QAndroidJniObject("android/bluetooth/BluetoothGattDescriptor", + "(Ljava/util/UUID;I)V", + javaUuidfromQtUuid(descData.uuid()).object(), + setupDescPermissions(descData)); + + jb = env->NewByteArray(descData.value().size()); + env->SetByteArrayRegion(jb, 0, descData.value().size(), (jbyte*)descData.value().data()); + success = javaDesc.callMethod<jboolean>("setValue", "([B)Z", 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", + "(Landroid/bluetooth/BluetoothGattDescriptor;)Z", + javaDesc.object()); + if (!success) { + qCWarning(QT_BT_ANDROID) << "Cannot add descriptor" << descData.uuid() + << "to service" << service->uuid << "(char:" + << charData.uuid() << ")"; + } + } + + success = service->androidService.callMethod<jboolean>( + "addCharacteristic", + "(Landroid/bluetooth/BluetoothGattCharacteristic;)Z", javaChar.object()); + if (!success) { + qCWarning(QT_BT_ANDROID) << "Cannot add characteristic" << charData.uuid() + << "to service" << service->uuid; + } + } + + hub->javaObject().callMethod<void>("addService", + "(Landroid/bluetooth/BluetoothGattService;)V", + service->androidService.object()); +} + QT_END_NAMESPACE |