summaryrefslogtreecommitdiffstats
path: root/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java')
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java987
1 files changed, 963 insertions, 24 deletions
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
index 0fd73613..f6c41e31 100644
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
+++ b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
@@ -1,7 +1,7 @@
/****************************************************************************
**
- ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
- ** Contact: http://www.qt-project.org/legal
+ ** Copyright (C) 2015 The Qt Company Ltd.
+ ** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtBluetooth module of the Qt Toolkit.
**
@@ -10,9 +10,9 @@
** 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 Digia. For licensing terms and
- ** conditions see http://qt.digia.com/licensing. For further information
- ** use the contact form at http://qt.digia.com/contact-us.
+ ** 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.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
@@ -23,8 +23,8 @@
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
- ** In addition, as a special exception, Digia gives you certain additional
- ** rights. These rights are described in the Digia Qt LGPL Exception
+ ** 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.
**
** $QT_END_LICENSE$
@@ -38,27 +38,35 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.util.Log;
import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.LinkedList;
import java.util.List;
+import java.util.UUID;
public class QtBluetoothLE {
private static final String TAG = "QtBluetoothGatt";
- private BluetoothAdapter mBluetoothAdapter;
+ private final BluetoothAdapter mBluetoothAdapter;
private boolean mLeScanRunning = false;
private BluetoothGatt mBluetoothGatt = null;
private String mRemoteGattAddress;
- private BluetoothDevice mRemoteGattDevice = null;
+ private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
/* Pointer to the Qt object that "owns" the Java object */
+ @SuppressWarnings({"CanBeFinal", "WeakerAccess"})
long qtObject = 0;
+ @SuppressWarnings("WeakerAccess")
Activity qtactivity = null;
+ @SuppressWarnings("WeakerAccess")
public QtBluetoothLE() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
@@ -69,6 +77,11 @@ public class QtBluetoothLE {
mRemoteGattAddress = remoteAddress;
}
+
+ /*************************************************************/
+ /* Device scan */
+ /*************************************************************/
+
/*
Returns true, if request was successfully completed
*/
@@ -87,7 +100,7 @@ public class QtBluetoothLE {
}
// Device scan callback
- private BluetoothAdapter.LeScanCallback leScanCallback =
+ private final BluetoothAdapter.LeScanCallback leScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
@@ -101,7 +114,11 @@ public class QtBluetoothLE {
public native void leScanResult(long qtObject, BluetoothDevice device, int rssi);
- private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
+ /*************************************************************/
+ /* Service Discovery */
+ /*************************************************************/
+
+ private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (qtObject == 0)
@@ -111,7 +128,13 @@ public class QtBluetoothLE {
//This must be in sync with QLowEnergyController::ControllerState
switch (newState) {
case BluetoothProfile.STATE_DISCONNECTED:
- qLowEnergyController_State = 0; break;
+ qLowEnergyController_State = 0;
+ // we disconnected -> get rid of data from previous run
+ resetData();
+ // reset mBluetoothGatt, reusing same object is not very reliable
+ // sometimes it reconnects and sometimes it does not.
+ mBluetoothGatt = null;
+ break;
case BluetoothProfile.STATE_CONNECTED:
qLowEnergyController_State = 2;
}
@@ -137,7 +160,7 @@ public class QtBluetoothLE {
errorCode = 0; //QLowEnergyController::NoError
final List<BluetoothGattService> services = mBluetoothGatt.getServices();
for (BluetoothGattService service: services) {
- builder.append(service.getUuid().toString() + " "); //space is separator
+ builder.append(service.getUuid().toString()).append(" "); //space is separator
}
break;
default:
@@ -146,24 +169,244 @@ public class QtBluetoothLE {
}
leServicesDiscovered(qtObject, errorCode, builder.toString());
}
+
+ public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ int status)
+ {
+ //runningHandle is only used during serviceDetailsDiscovery
+ //If it is -1 we got an update outside of the details discovery process
+ final boolean isServiceDiscoveryRun = (runningHandle != -1);
+
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "onCharacteristicRead error: " + status);
+
+ // read errors during serviceDiscovery are ignored
+ if (isServiceDiscoveryRun)
+ return;
+ }
+
+ synchronized (this) {
+ if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready;
+ return;
+ }
+
+ // once we have a service discovery run we report regular changes
+ if (!isServiceDiscoveryRun) {
+
+ int foundHandle = -1;
+ synchronized (this) {
+ foundHandle = handleForCharacteristic(characteristic);
+ }
+
+ synchronized (readWriteQueue) {
+ ioJobPending = false;
+ }
+
+ if (foundHandle == -1) {
+ Log.w(TAG, "Out-of-detail-discovery: char update failed. " +
+ "Cannot find handle for characteristic");
+ } else {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(),
+ foundHandle + 1, characteristic.getUuid().toString(),
+ characteristic.getProperties(), characteristic.getValue());
+ } else {
+ // This must be in sync with QLowEnergyService::CharacteristicReadError
+ final int characteristicReadError = 5;
+ leServiceError(qtObject, foundHandle + 1, characteristicReadError);
+ }
+ }
+
+ performNextIO();
+ return;
+ }
+
+ GattEntry entry = entries.get(runningHandle);
+ entry.valueKnown = true;
+ entries.set(runningHandle, entry);
+
+ // Qt manages handles starting at 1, in Java we use a system starting with 0
+ //TODO avoid sending service uuid -> service handle should be sufficient
+ leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(),
+ runningHandle + 1, characteristic.getUuid().toString(),
+ characteristic.getProperties(), characteristic.getValue());
+ performServiceDetailDiscoveryForHandle(runningHandle + 1, false);
+ }
+
+ public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ int status)
+ {
+ if (status != BluetoothGatt.GATT_SUCCESS)
+ Log.w(TAG, "onCharacteristicWrite: error " + status);
+
+ int handle = handleForCharacteristic(characteristic);
+ if (handle == -1) {
+ Log.w(TAG,"onCharacteristicWrite: cannot find handle");
+ return;
+ }
+
+ int errorCode;
+ //This must be in sync with QLowEnergyService::ServiceError
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ errorCode = 0; break; // NoError
+ default:
+ errorCode = 2; break; // CharacteristicWriteError
+ }
+
+ synchronized (readWriteQueue) {
+ ioJobPending = false;
+ }
+ leCharacteristicWritten(qtObject, handle+1, characteristic.getValue(), errorCode);
+ performNextIO();
+ }
+
+ public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic)
+ {
+ int handle = handleForCharacteristic(characteristic);
+ if (handle == -1) {
+ Log.w(TAG,"onCharacteristicChanged: cannot find handle");
+ return;
+ }
+
+ leCharacteristicChanged(qtObject, handle+1, characteristic.getValue());
+ }
+
+ public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status)
+ {
+ //runningHandle is only used during serviceDetailsDiscovery
+ //If it is -1 we got an update outside of the details discovery process
+ final boolean isServiceDiscoveryRun = (runningHandle != -1);
+
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "onDescriptorRead error: " + status);
+
+ // read errors during serviceDiscovery are ignored
+ if (isServiceDiscoveryRun)
+ return;
+ }
+
+ synchronized (this) {
+ if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready;
+ return;
+ }
+
+
+ if (!isServiceDiscoveryRun) {
+
+ int foundHandle = -1;
+ synchronized (this) {
+ foundHandle = handleForDescriptor(descriptor);
+ }
+
+ synchronized (readWriteQueue) {
+ ioJobPending = false;
+ }
+
+ if (foundHandle == -1) {
+ Log.w(TAG, "Out-of-detail-discovery: char update failed. " +
+ "Cannot find handle for descriptor.");
+ } else {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(),
+ descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1,
+ descriptor.getUuid().toString(), descriptor.getValue());
+ } else {
+ // This must be in sync with QLowEnergyService::DescriptorReadError
+ final int descriptorReadError = 6;
+ leServiceError(qtObject, foundHandle + 1, descriptorReadError);
+ }
+ }
+
+ performNextIO();
+ return;
+ }
+
+
+ GattEntry entry = entries.get(runningHandle);
+ entry.valueKnown = true;
+ entries.set(runningHandle, entry);
+ //TODO avoid sending service and characteristic uuid -> handles should be sufficient
+ leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(),
+ descriptor.getCharacteristic().getUuid().toString(), runningHandle+1,
+ descriptor.getUuid().toString(), descriptor.getValue());
+
+ /* Some devices preset ClientCharacteristicConfiguration descriptors
+ * to enable notifications out of the box. However the additional
+ * BluetoothGatt.setCharacteristicNotification call prevents
+ * automatic notifications from coming through. Hence we manually set them
+ * up here.
+ */
+
+ if (descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) {
+ final int value = descriptor.getValue()[0];
+ // notification or indication bit set?
+ if ((value & 0x03) > 0) {
+ Log.d(TAG, "Found descriptor with automatic notifications.");
+ mBluetoothGatt.setCharacteristicNotification(
+ descriptor.getCharacteristic(), true);
+ }
+ }
+
+ performServiceDetailDiscoveryForHandle(runningHandle + 1, false);
+ }
+
+ public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status)
+ {
+ if (status != BluetoothGatt.GATT_SUCCESS)
+ Log.w(TAG, "onDescriptorWrite: error " + status);
+
+ int handle = handleForDescriptor(descriptor);
+
+ int errorCode;
+ //This must be in sync with QLowEnergyService::ServiceError
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ errorCode = 0; break; // NoError
+ default:
+ errorCode = 3; break; // DescriptorWriteError
+ }
+
+ synchronized (readWriteQueue) {
+ ioJobPending = false;
+ }
+
+ leDescriptorWritten(qtObject, handle+1, descriptor.getValue(), errorCode);
+ performNextIO();
+ }
+ //TODO Requires Android API 21 which is not available on CI yet.
+// public void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt,
+// int status) {
+// System.out.println("onReliableWriteCompleted");
+// }
+//
+// public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt,
+// int rssi, int status) {
+// System.out.println("onReadRemoteRssi");
+// }
+
};
- public native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState);
- public native void leServicesDiscovered(long qtObject, int errorCode, String uuidList);
public boolean connect() {
- if (mBluetoothGatt != null)
- return mBluetoothGatt.connect();
+ BluetoothDevice mRemoteGattDevice;
- mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
- if (mRemoteGattDevice == null)
+ try {
+ mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Remote address is not valid: " + mRemoteGattAddress);
return false;
+ }
mBluetoothGatt = mRemoteGattDevice.connectGatt(qtactivity, false, gattCallback);
- if (mBluetoothGatt == null)
- return false;
-
- return true;
+ return mBluetoothGatt != null;
}
public void disconnect() {
@@ -175,11 +418,707 @@ public class QtBluetoothLE {
public boolean discoverServices()
{
+ return mBluetoothGatt != null && mBluetoothGatt.discoverServices();
+ }
+
+ private enum GattEntryType
+ {
+ Service, Characteristic, CharacteristicValue, Descriptor
+ }
+ private class GattEntry
+ {
+ public GattEntryType type;
+ public boolean valueKnown = false;
+ public BluetoothGattService service = null;
+ public BluetoothGattCharacteristic characteristic = null;
+ public BluetoothGattDescriptor descriptor = null;
+ public int endHandle;
+ }
+
+ private enum IoJobType
+ {
+ Read, Write
+ }
+
+ private class ReadWriteJob
+ {
+ public GattEntry entry;
+ public byte[] newValue;
+ public int requestedWriteType;
+ public IoJobType jobType;
+ }
+
+ private final Hashtable<UUID, List<Integer>> uuidToEntry = new Hashtable<UUID, List<Integer>>(100);
+ private final ArrayList<GattEntry> entries = new ArrayList<GattEntry>(100);
+ private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>();
+
+
+ private final LinkedList<ReadWriteJob> readWriteQueue = new LinkedList<ReadWriteJob>();
+ private boolean ioJobPending;
+
+ /*
+ Internal helper function
+ Returns the handle id for the given characteristic; otherwise returns -1.
+
+ Note that this is the Java handle. The Qt handle is the Java handle +1.
+ */
+ private int handleForCharacteristic(BluetoothGattCharacteristic characteristic)
+ {
+ if (characteristic == null)
+ return -1;
+
+ List<Integer> handles = uuidToEntry.get(characteristic.getService().getUuid());
+ if (handles == null || handles.isEmpty())
+ return -1;
+
+ //TODO for now we assume we always want the first service in case of uuid collision
+ int serviceHandle = handles.get(0);
+
+ try {
+ GattEntry entry;
+ for (int i = serviceHandle+1; i < entries.size(); i++) {
+ entry = entries.get(i);
+ if (entry == null)
+ continue;
+
+ switch (entry.type) {
+ case Descriptor:
+ case CharacteristicValue:
+ continue;
+ case Service:
+ break;
+ case Characteristic:
+ if (entry.characteristic == characteristic)
+ return i;
+ break;
+ }
+ }
+ } catch (IndexOutOfBoundsException ex) { /*nothing*/ }
+ return -1;
+ }
+
+ /*
+ Internal helper function
+ Returns the handle id for the given descriptor; otherwise returns -1.
+
+ Note that this is the Java handle. The Qt handle is the Java handle +1.
+ */
+ private int handleForDescriptor(BluetoothGattDescriptor descriptor)
+ {
+ if (descriptor == null)
+ return -1;
+
+ List<Integer> handles = uuidToEntry.get(descriptor.getCharacteristic().getService().getUuid());
+ if (handles == null || handles.isEmpty())
+ return -1;
+
+ //TODO for now we assume we always want the first service in case of uuid collision
+ int serviceHandle = handles.get(0);
+
+ try {
+ GattEntry entry;
+ for (int i = serviceHandle+1; i < entries.size(); i++) {
+ entry = entries.get(i);
+ if (entry == null)
+ continue;
+
+ switch (entry.type) {
+ case Characteristic:
+ case CharacteristicValue:
+ continue;
+ case Service:
+ break;
+ case Descriptor:
+ if (entry.descriptor == descriptor)
+ return i;
+ break;
+ }
+ }
+ } catch (IndexOutOfBoundsException ignored) { }
+ return -1;
+ }
+
+ private void populateHandles()
+ {
+ // We introduce the notion of artificial handles. While GATT handles
+ // are not exposed on Android they help to quickly identify GATT attributes
+ // on the C++ side. The Qt Api will not expose the handles
+ GattEntry entry = null;
+ List<BluetoothGattService> services = mBluetoothGatt.getServices();
+ for (BluetoothGattService service: services) {
+ GattEntry serviceEntry = new GattEntry();
+ serviceEntry.type = GattEntryType.Service;
+ serviceEntry.service = service;
+ entries.add(entry);
+
+ // remember handle for the service for later update
+ int serviceHandle = entries.size() - 1;
+
+ //some devices may have more than one service with the same uuid
+ List<Integer> old = uuidToEntry.get(service.getUuid());
+ if (old == null)
+ old = new ArrayList<Integer>();
+ old.add(entries.size()-1);
+ uuidToEntry.put(service.getUuid(), old);
+
+ // add all characteristics
+ List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
+ for (BluetoothGattCharacteristic characteristic: charList) {
+ entry = new GattEntry();
+ entry.type = GattEntryType.Characteristic;
+ entry.characteristic = characteristic;
+ entries.add(entry);
+
+ // this emulates GATT value attributes
+ entry = new GattEntry();
+ entry.type = GattEntryType.CharacteristicValue;
+ entries.add(entry);
+
+ // add all descriptors
+ List<BluetoothGattDescriptor> descList = characteristic.getDescriptors();
+ for (BluetoothGattDescriptor desc: descList) {
+ entry = new GattEntry();
+ entry.type = GattEntryType.Descriptor;
+ entry.descriptor = desc;
+ entries.add(entry);
+ }
+ }
+
+ // update endHandle of current service
+ serviceEntry.endHandle = entries.size() - 1;
+ entries.set(serviceHandle, serviceEntry);
+ }
+
+ entries.trimToSize();
+ }
+
+ private int currentServiceInDiscovery = -1;
+ private int runningHandle = -1;
+
+ private void resetData()
+ {
+ synchronized (this) {
+ runningHandle = -1;
+ currentServiceInDiscovery = -1;
+ uuidToEntry.clear();
+ entries.clear();
+ servicesToBeDiscovered.clear();
+ }
+ synchronized (readWriteQueue) {
+ readWriteQueue.clear();
+ }
+ }
+
+ public synchronized boolean discoverServiceDetails(String serviceUuid)
+ {
+ try {
+ if (mBluetoothGatt == null)
+ return false;
+
+ if (entries.isEmpty())
+ populateHandles();
+
+ GattEntry entry;
+ int serviceHandle;
+ try {
+ UUID service = UUID.fromString(serviceUuid);
+ List<Integer> handles = uuidToEntry.get(service);
+ if (handles == null || handles.isEmpty()) {
+ Log.w(TAG, "Unknown service uuid for current device: " + service.toString());
+ return false;
+ }
+
+ //TODO for now we assume we always want the first service in case of uuid collision
+ serviceHandle = handles.get(0);
+ entry = entries.get(serviceHandle);
+ if (entry == null) {
+ Log.w(TAG, "Service with UUID " + service.toString() + " not found");
+ return false;
+ }
+ } catch (IllegalArgumentException ex) {
+ //invalid UUID string passed
+ Log.w(TAG, "Cannot parse given UUID");
+ return false;
+ }
+
+ if (entry.type != GattEntryType.Service) {
+ Log.w(TAG, "Given UUID is not a service UUID: " + serviceUuid);
+ return false;
+ }
+
+ // current service already under investigation
+ if (currentServiceInDiscovery == serviceHandle)
+ return true;
+
+ if (currentServiceInDiscovery != -1) {
+ // we are currently discovering another service
+ // we queue the new one up until we finish the previous one
+ if (!entry.valueKnown) {
+ servicesToBeDiscovered.add(serviceHandle);
+ Log.w(TAG, "Service discovery already running on another service, " +
+ "queueing request for " + serviceUuid);
+ } else {
+ Log.w(TAG, "Service already known");
+ }
+ return true;
+ }
+
+ if (!entry.valueKnown) {
+ performServiceDetailDiscoveryForHandle(serviceHandle, true);
+ } else {
+ Log.w(TAG, "Service already discovered");
+ }
+
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ Returns the uuids of the services included by the given service. Otherwise returns null.
+ Directly called from Qt.
+ */
+ public String includedServices(String serviceUuid)
+ {
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(serviceUuid);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return null;
+ }
+
+ //TODO Breaks in case of two services with same uuid
+ BluetoothGattService service = mBluetoothGatt.getService(uuid);
+ if (service == null)
+ return null;
+
+ final List<BluetoothGattService> includes = service.getIncludedServices();
+ if (includes.isEmpty())
+ return null;
+
+ StringBuilder builder = new StringBuilder();
+ for (BluetoothGattService includedService: includes) {
+ builder.append(includedService.getUuid().toString()).append(" "); //space is separator
+ }
+
+ return builder.toString();
+ }
+
+ private void finishCurrentServiceDiscovery()
+ {
+ int currentEntry = currentServiceInDiscovery;
+ GattEntry discoveredService = entries.get(currentServiceInDiscovery);
+ discoveredService.valueKnown = true;
+ entries.set(currentServiceInDiscovery, discoveredService);
+
+ runningHandle = -1;
+ currentServiceInDiscovery = -1;
+
+ leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(),
+ currentEntry + 1, discoveredService.endHandle + 1);
+
+ if (!servicesToBeDiscovered.isEmpty()) {
+ try {
+ int nextService = servicesToBeDiscovered.remove();
+ performServiceDetailDiscoveryForHandle(nextService, true);
+ } catch (IndexOutOfBoundsException ex) {
+ Log.w(TAG, "Expected queued service but didn't find any");
+ }
+ }
+ }
+
+ private synchronized void performServiceDetailDiscoveryForHandle(int nextHandle, boolean searchStarted)
+ {
+ try {
+ if (searchStarted) {
+ currentServiceInDiscovery = nextHandle;
+ runningHandle = ++nextHandle;
+ } else {
+ runningHandle = nextHandle;
+ }
+
+ GattEntry entry;
+ try {
+ entry = entries.get(nextHandle);
+ } catch (IndexOutOfBoundsException ex) {
+ //ex.printStackTrace();
+ Log.w(TAG, "Last entry of last service read");
+ finishCurrentServiceDiscovery();
+ return;
+ }
+
+ boolean result;
+ switch (entry.type) {
+ case Characteristic:
+ result = mBluetoothGatt.readCharacteristic(entry.characteristic);
+ try {
+ if (!result) {
+ // add characteristic now since we won't get a read update later one
+ // this is possible when the characteristic is not readable
+ Log.d(TAG, "Non-readable characteristic " + entry.characteristic.getUuid() +
+ " for service " + entry.characteristic.getService().getUuid());
+ leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(),
+ nextHandle + 1, entry.characteristic.getUuid().toString(),
+ entry.characteristic.getProperties(), entry.characteristic.getValue());
+ performServiceDetailDiscoveryForHandle(runningHandle + 1, false);
+ }
+ } catch (Exception ex)
+ {
+ ex.printStackTrace();
+ }
+ break;
+ case CharacteristicValue:
+ // ignore -> nothing to do for this artificial type
+ performServiceDetailDiscoveryForHandle(runningHandle + 1, false);
+ break;
+ case Descriptor:
+ result = mBluetoothGatt.readDescriptor(entry.descriptor);
+ if (!result) {
+ // atm all descriptor types are readable
+ Log.d(TAG, "Non-readable descriptor " + entry.descriptor.getUuid() +
+ " for service/char" + entry.descriptor.getCharacteristic().getService().getUuid() +
+ "/" + entry.descriptor.getCharacteristic().getUuid());
+ leDescriptorRead(qtObject,
+ entry.descriptor.getCharacteristic().getService().getUuid().toString(),
+ entry.descriptor.getCharacteristic().getUuid().toString(),
+ nextHandle+1, entry.descriptor.getUuid().toString(),
+ entry.descriptor.getValue());
+ performServiceDetailDiscoveryForHandle(runningHandle + 1, false);
+ }
+ break;
+ case Service:
+ finishCurrentServiceDiscovery();
+ break;
+ default:
+ Log.w(TAG, "Invalid GATT attribute type");
+ break;
+ }
+
+ } catch(Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ /*************************************************************/
+ /* Write Characteristics */
+ /*************************************************************/
+
+ public boolean writeCharacteristic(int charHandle, byte[] newValue,
+ int writeMode)
+ {
+ if (mBluetoothGatt == null)
+ return false;
+
+ GattEntry entry;
+ try {
+ entry = entries.get(charHandle-1); //Qt always uses handles+1
+ } catch (IndexOutOfBoundsException ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.newValue = newValue;
+ newJob.entry = entry;
+ newJob.jobType = IoJobType.Write;
+
+ // writeMode must be in sync with QLowEnergyService::WriteMode
+ // For now we ignore SignedWriteType as Qt doesn't support it yet.
+ switch (writeMode) {
+ case 1: //WriteWithoutResponse
+ newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
+ break;
+ default:
+ newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
+ break;
+ }
+
+ boolean result;
+ synchronized (readWriteQueue) {
+ result = readWriteQueue.add(newJob);
+ }
+
+ if (!result) {
+ Log.w(TAG, "Cannot add characteristic write request for " + charHandle + " to queue" );
+ return false;
+ }
+
+ performNextIO();
+ return true;
+ }
+
+ /*************************************************************/
+ /* Write Descriptors */
+ /*************************************************************/
+
+ public boolean writeDescriptor(int descHandle, byte[] newValue)
+ {
+ if (mBluetoothGatt == null)
+ return false;
+
+ GattEntry entry;
+ try {
+ entry = entries.get(descHandle-1); //Qt always uses handles+1
+ } catch (IndexOutOfBoundsException ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.newValue = newValue;
+ newJob.entry = entry;
+ newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
+ newJob.jobType = IoJobType.Write;
+
+ boolean result;
+ synchronized (readWriteQueue) {
+ result = readWriteQueue.add(newJob);
+ }
+
+ if (!result) {
+ Log.w(TAG, "Cannot add descriptor write request for " + descHandle + " to queue" );
+ return false;
+ }
+
+ performNextIO();
+ return true;
+ }
+
+ /*************************************************************/
+ /* Read Characteristics */
+ /*************************************************************/
+
+ public boolean readCharacteristic(int charHandle)
+ {
if (mBluetoothGatt == null)
return false;
- return mBluetoothGatt.discoverServices();
+ GattEntry entry;
+ try {
+ entry = entries.get(charHandle-1); //Qt always uses handles+1
+ } catch (IndexOutOfBoundsException ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.entry = entry;
+ newJob.jobType = IoJobType.Read;
+
+ boolean result;
+ synchronized (readWriteQueue) {
+ result = readWriteQueue.add(newJob);
+ }
+
+ if (!result) {
+ Log.w(TAG, "Cannot add characteristic read request for " + charHandle + " to queue" );
+ return false;
+ }
+
+ performNextIO();
+ return true;
+ }
+
+ public boolean readDescriptor(int descHandle)
+ {
+ if (mBluetoothGatt == null)
+ return false;
+
+ GattEntry entry;
+ try {
+ entry = entries.get(descHandle-1); //Qt always uses handles+1
+ } catch (IndexOutOfBoundsException ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.entry = entry;
+ newJob.jobType = IoJobType.Read;
+
+ boolean result;
+ synchronized (readWriteQueue) {
+ result = readWriteQueue.add(newJob);
+ }
+
+ if (!result) {
+ Log.w(TAG, "Cannot add descriptor read request for " + descHandle + " to queue" );
+ return false;
+ }
+
+ performNextIO();
+ return true;
+ }
+
+ /*
+ The queuing is required because two writeCharacteristic/writeDescriptor calls
+ cannot execute at the same time. The second write must happen after the
+ previous write has finished with on(Characteristic|Descriptor)Write().
+ */
+ private void performNextIO()
+ {
+ if (mBluetoothGatt == null)
+ return;
+
+ boolean skip = false;
+ final ReadWriteJob nextJob;
+ synchronized (readWriteQueue) {
+ if (readWriteQueue.isEmpty() || ioJobPending)
+ return;
+
+ nextJob = readWriteQueue.remove();
+
+ Log.w(TAG, "Performing queued job " + nextJob.jobType);
+ if (nextJob.jobType == IoJobType.Read)
+ skip = executeReadJob(nextJob);
+ else
+ skip = executeWriteJob(nextJob);
+
+ if (!skip)
+ ioJobPending = true;
+ }
+
+ if (skip) {
+ Log.w(TAG, "Skipping: " + nextJob.entry.type);
+
+ /*
+ BluetoothGatt.[read|write][Characteristic|Descriptor]() immediately
+ return in cases where meta data doesn't match the intended action
+ (e.g. trying to write to read-only char). When this happens
+ we have to report an error back to Qt. This is not required during
+ the initial service discovery though.
+ */
+ final boolean isServiceDiscoveryRun = (runningHandle != -1);
+ if (!isServiceDiscoveryRun) {
+ int handle = -1;
+ if (nextJob.entry.type == GattEntryType.Characteristic)
+ handle = handleForCharacteristic(nextJob.entry.characteristic);
+ else
+ handle = handleForDescriptor(nextJob.entry.descriptor);
+
+ if (handle != -1) {
+ int errorCode = 0;
+
+ // The error codes below must be in sync with QLowEnergyService::ServiceError
+ if (nextJob.jobType == IoJobType.Read) {
+ errorCode = (nextJob.entry.type == GattEntryType.Characteristic) ?
+ 5 : 6; // CharacteristicReadError : DescriptorReadError
+ } else {
+ errorCode = (nextJob.entry.type == GattEntryType.Characteristic) ?
+ 2 : 3; // CharacteristicWriteError : DescriptorWriteError
+ }
+
+ leServiceError(qtObject, handle + 1, errorCode);
+ }
+ }
+
+ performNextIO();
+ }
}
+ // Runs inside the Mutex on readWriteQueue.
+ // Returns true if nextJob should be skipped.
+ private boolean executeWriteJob(ReadWriteJob nextJob)
+ {
+ boolean result;
+ switch (nextJob.entry.type) {
+ case Characteristic:
+ if (nextJob.entry.characteristic.getWriteType() != nextJob.requestedWriteType) {
+ nextJob.entry.characteristic.setWriteType(nextJob.requestedWriteType);
+ }
+ result = nextJob.entry.characteristic.setValue(nextJob.newValue);
+ if (!result || !mBluetoothGatt.writeCharacteristic(nextJob.entry.characteristic))
+ return true;
+ break;
+ case Descriptor:
+ if (nextJob.entry.descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) {
+ /*
+ For some reason, Android splits characteristic notifications
+ into two operations. BluetoothGatt.enableCharacteristicNotification
+ ensures the local Bluetooth stack forwards the notifications. In addition,
+ BluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
+ must be written to the peripheral.
+ */
+
+
+ /* There is no documentation on indication behavior. The assumption is
+ that when indication or notification are requested we call
+ BluetoothGatt.setCharacteristicNotification. Furthermore it is assumed
+ indications are send via onCharacteristicChanged too and Android itself
+ will do the confirmation required for an indication as per
+ Bluetooth spec Vol 3, Part G, 4.11 . If neither of the two bits are set
+ we disable the signals.
+ */
+ boolean enableNotifications = false;
+ int value = (nextJob.newValue[0] & 0xff);
+ // first or second bit must be set
+ if (((value & 0x1) == 1) || (((value >> 1) & 0x1) == 1)) {
+ enableNotifications = true;
+ }
+
+ result = mBluetoothGatt.setCharacteristicNotification(
+ nextJob.entry.descriptor.getCharacteristic(), enableNotifications);
+ if (!result) {
+ Log.w(TAG, "Cannot set characteristic notification");
+ //we continue anyway to ensure that we write the requested value
+ //to the device
+ }
+
+ Log.d(TAG, "Enable notifications: " + enableNotifications);
+ }
+
+ result = nextJob.entry.descriptor.setValue(nextJob.newValue);
+ if (!result || !mBluetoothGatt.writeDescriptor(nextJob.entry.descriptor))
+ return true;
+ break;
+ case Service:
+ case CharacteristicValue:
+ return true;
+ }
+ return false;
+ }
+
+ // Runs inside the Mutex on readWriteQueue.
+ // Returns true if nextJob should be skipped.
+ private boolean executeReadJob(ReadWriteJob nextJob)
+ {
+ boolean result;
+ switch (nextJob.entry.type) {
+ case Characteristic:
+ result = mBluetoothGatt.readCharacteristic(nextJob.entry.characteristic);
+ if (!result)
+ return true; // skip
+ break;
+ case Descriptor:
+ result = mBluetoothGatt.readDescriptor(nextJob.entry.descriptor);
+ if (!result)
+ return true; // skip
+ break;
+ case Service:
+ case CharacteristicValue:
+ return true;
+ }
+ return false;
+ }
+
+ public native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState);
+ public native void leServicesDiscovered(long qtObject, int errorCode, String uuidList);
+ public native void leServiceDetailDiscoveryFinished(long qtObject, final String serviceUuid,
+ int startHandle, int endHandle);
+ public native void leCharacteristicRead(long qtObject, String serviceUuid,
+ int charHandle, String charUuid,
+ int properties, byte[] data);
+ public native void leDescriptorRead(long qtObject, String serviceUuid, String charUuid,
+ int descHandle, String descUuid, byte[] data);
+ public native void leCharacteristicWritten(long qtObject, int charHandle, byte[] newData,
+ int errorCode);
+ public native void leDescriptorWritten(long qtObject, int charHandle, byte[] newData,
+ int errorCode);
+ public native void leCharacteristicChanged(long qtObject, int charHandle, byte[] newData);
+ public native void leServiceError(long qtObject, int attributeHandle, int errorCode);
}