blob: 86e6ade79f9c7b00b6a1d911d7e0d4c62d44c220 [file] [log] [blame]
/****************************************************************************
**
** 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 "qlowenergycontroller_android_p.h"
#include <QtCore/QLoggingCategory>
#include <QtAndroidExtras/QAndroidJniEnvironment>
#include <QtAndroidExtras/QAndroidJniObject>
#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;
}
QLowEnergyControllerPrivateAndroid::QLowEnergyControllerPrivateAndroid()
: QLowEnergyControllerPrivate(),
hub(0)
{
registerQLowEnergyControllerMetaType();
}
QLowEnergyControllerPrivateAndroid::~QLowEnergyControllerPrivateAndroid()
{
if (role == QLowEnergyController::PeripheralRole) {
if (hub)
hub->javaObject().callMethod<void>("disconnectServer");
}
}
void QLowEnergyControllerPrivateAndroid::init()
{
// 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;
}
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, &QLowEnergyControllerPrivateAndroid::connectionUpdated);
connect(hub, &LowEnergyNotificationHub::advertisementError,
this, &QLowEnergyControllerPrivateAndroid::advertisementError);
connect(hub, &LowEnergyNotificationHub::serverCharacteristicChanged,
this, &QLowEnergyControllerPrivateAndroid::serverCharacteristicChanged);
connect(hub, &LowEnergyNotificationHub::serverDescriptorWritten,
this, &QLowEnergyControllerPrivateAndroid::serverDescriptorWritten);
} else {
if (version < 18) {
qWarning() << "Qt Bluetooth LE Central/Client support not available"
"on Android devices below version 18";
return;
}
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::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);
}
}
void QLowEnergyControllerPrivateAndroid::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";
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()
{
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)
{
if (!serviceList.contains(service)) {
qCWarning(QT_BT_ANDROID) << "Discovery of unknown service" << service.toString()
<< "not possible";
return;
}
if (!hub)
return;
//cut leading { and trailing } {xxx-xxx}
QString tempUuid = service.toString();
tempUuid.chop(1); //remove trailing '}'
tempUuid.remove(0, 1); //remove first '{'
QAndroidJniEnvironment env;
QAndroidJniObject uuid = QAndroidJniObject::fromString(tempUuid);
bool result = hub->javaObject().callMethod<jboolean>("discoverServiceDetails",
"(Ljava/lang/String;)Z",
uuid.object<jstring>());
if (!result) {
QSharedPointer<QLowEnergyServicePrivate> servicePrivate =
serviceList.value(service);
if (!servicePrivate.isNull()) {
servicePrivate->setError(QLowEnergyService::UnknownError);
servicePrivate->setState(QLowEnergyService::DiscoveryRequired);
}
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;
QAndroidJniEnvironment env;
jbyteArray payload;
payload = env->NewByteArray(newValue.size());
env->SetByteArrayRegion(payload, 0, newValue.size(),
(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", "(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()) {
env->ExceptionDescribe();
env->ExceptionClear();
result = false;
}
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());
QAndroidJniEnvironment env;
jbyteArray payload;
payload = env->NewByteArray(newValue.size());
env->SetByteArrayRegion(payload, 0, newValue.size(),
(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", "(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()) {
env->ExceptionDescribe();
env->ExceptionClear();
result = false;
}
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;
QAndroidJniEnvironment env;
bool result = false;
if (hub) {
qCDebug(QT_BT_ANDROID) << "Read characteristic with handle"
<< charHandle << service->uuid;
result = hub->javaObject().callMethod<jboolean>("readCharacteristic",
"(I)Z", charHandle);
}
if (env->ExceptionOccurred()) {
env->ExceptionDescribe();
env->ExceptionClear();
result = false;
}
if (!result)
service->setError(QLowEnergyService::CharacteristicReadError);
}
void QLowEnergyControllerPrivateAndroid::readDescriptor(
const QSharedPointer<QLowEnergyServicePrivate> service,
const QLowEnergyHandle /*charHandle*/,
const QLowEnergyHandle descriptorHandle)
{
Q_ASSERT(!service.isNull());
QAndroidJniEnvironment env;
bool result = false;
if (hub) {
qCDebug(QT_BT_ANDROID) << "Read descriptor with handle"
<< descriptorHandle << service->uuid;
result = hub->javaObject().callMethod<jboolean>("readDescriptor",
"(I)Z", descriptorHandle);
}
if (env->ExceptionOccurred()) {
env->ExceptionDescribe();
env->ExceptionClear();
result = false;
}
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);
}
// 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();
Q_Q(QLowEnergyController);
if (oldState == QLowEnergyController::ConnectedState
&& newState != QLowEnergyController::ConnectedState) {
remoteDevice.clear();
remoteName.clear();
emit q->disconnected();
} else if (newState == QLowEnergyController::ConnectedState
&& oldState != QLowEnergyController::ConnectedState) {
if (hub) {
remoteDevice = QBluetoothAddress(hub->javaObject().callObjectMethod<jstring>("remoteAddress").toString());
remoteName = hub->javaObject().callObjectMethod<jstring>("remoteName").toString();
}
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(QStringLiteral(" "), QString::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()) {
QAndroidJniObject uuid = QAndroidJniObject::fromString(serviceUuid);
QAndroidJniObject javaIncludes = hub->javaObject().callObjectMethod(
"includedServices",
"(Ljava/lang/String;)Ljava/lang/String;",
uuid.object<jstring>());
if (javaIncludes.isValid()) {
const QStringList list = javaIncludes.toString()
.split(QStringLiteral(" "),
QString::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::ServiceDiscovered);
}
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::ServiceDiscovered) {
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::ServiceDiscovered){
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 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 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();
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 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 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->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 &params)
{
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 QLowEnergyControllerPrivateAndroid::startAdvertising(const QLowEnergyAdvertisingParameters &params,
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 QLowEnergyControllerPrivateAndroid::stopAdvertising()
{
setState(QLowEnergyController::UnconnectedState);
hub->javaObject().callMethod<void>("stopAdvertising");
}
void QLowEnergyControllerPrivateAndroid::requestConnectionUpdate(const QLowEnergyConnectionParameters &params)
{
// 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 QLowEnergyControllerPrivateAndroid::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