diff options
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.java | 756 |
1 files changed, 502 insertions, 254 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 e5711426..35680c82 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,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$ ** @@ -33,7 +39,6 @@ package org.qtproject.qt5.android.bluetooth; -import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; @@ -42,14 +47,22 @@ import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; import android.util.Log; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; import java.util.UUID; + public class QtBluetoothLE { private static final String TAG = "QtBluetoothGatt"; private final BluetoothAdapter mBluetoothAdapter; @@ -59,21 +72,49 @@ public class QtBluetoothLE { private String mRemoteGattAddress; private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + /* + * The atomic synchronizes the timeoutRunnable thread and the response thread for the pending + * I/O job. Whichever thread comes first will pass the atomic gate. The other thread is + * cut short. + */ + private AtomicInteger handleForTimeout = new AtomicInteger(-1); // -1 implies not running + private final int RUNNABLE_TIMEOUT = 3000; // 3 seconds + private final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + + private class TimeoutRunnable implements Runnable { + public TimeoutRunnable(int handle) { pendingJobHandle = handle; } + @Override + public void run() { + boolean timeoutStillValid = handleForTimeout.compareAndSet(pendingJobHandle, -1); + if (timeoutStillValid) { + Log.w(TAG, "****** Timeout for request on handle " + (pendingJobHandle & 0xffff)); + Log.w(TAG, "****** Looks like the characteristic or descriptor does NOT act in " + + "accordance to Bluetooth 4.x spec."); + Log.w(TAG, "****** Please check server implementation. Continuing under " + + "reservation."); + interruptCurrentIO(pendingJobHandle & 0xffff); + } + } + + // contains handle (0xffff) and top 2 byte contain the job type (0xffff0000) + private int pendingJobHandle = -1; + }; + /* Pointer to the Qt object that "owns" the Java object */ @SuppressWarnings({"CanBeFinal", "WeakerAccess"}) long qtObject = 0; @SuppressWarnings("WeakerAccess") - Activity qtactivity = null; + Context qtContext = null; @SuppressWarnings("WeakerAccess") public QtBluetoothLE() { mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } - public QtBluetoothLE(final String remoteAddress, Activity activity) { + public QtBluetoothLE(final String remoteAddress, Context context) { this(); - qtactivity = activity; + qtContext = context; mRemoteGattAddress = remoteAddress; } @@ -108,11 +149,11 @@ public class QtBluetoothLE { if (qtObject == 0) return; - leScanResult(qtObject, device, rssi); + leScanResult(qtObject, device, rssi, scanRecord); } }; - public native void leScanResult(long qtObject, BluetoothDevice device, int rssi); + public native void leScanResult(long qtObject, BluetoothDevice device, int rssi, byte[] scanRecord); /*************************************************************/ /* Service Discovery */ @@ -146,8 +187,12 @@ public class QtBluetoothLE { switch (status) { case BluetoothGatt.GATT_SUCCESS: errorCode = 0; break; //QLowEnergyController::NoError + case 8: // BLE_HCI_CONNECTION_TIMEOUT + case 22: // BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION + Log.w(TAG, "Connection Error: Try to delay connect() call after previous activity"); + errorCode = 5; break; //QLowEnergyController::ConnectionError default: - Log.w(TAG, "Unhandled error code on connectionStateChanged: " + status); + Log.w(TAG, "Unhandled error code on connectionStateChanged: " + status + " " + newState); errorCode = status; break; //TODO deal with all errors } leConnectionStateChange(qtObject, errorCode, qLowEnergyController_State); @@ -176,64 +221,72 @@ public class QtBluetoothLE { 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); + int foundHandle = -1; + synchronized (this) { + foundHandle = handleForCharacteristic(characteristic); + if (foundHandle == -1 || foundHandle >= entries.size() ) { + Log.w(TAG, "Cannot find characteristic read request for read notification - handle: " + + foundHandle + " size: " + entries.size()); + + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } - // read errors during serviceDiscovery are ignored - if (isServiceDiscoveryRun) + performNextIO(); return; + } } - synchronized (this) { - if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready; - return; + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(foundHandle, IoJobType.Read), -1); + if (requestTimedOut) { + Log.w(TAG, "Late char read reply after timeout was hit for handle " + foundHandle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; } - // once we have a service discovery run we report regular changes - if (!isServiceDiscoveryRun) { + GattEntry entry = entries.get(foundHandle); + final boolean isServiceDiscoveryRun = !entry.valueKnown; + entry.valueKnown = true; - int foundHandle = -1; - synchronized (this) { - foundHandle = handleForCharacteristic(characteristic); + if (status == BluetoothGatt.GATT_SUCCESS) { + // 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(), + foundHandle + 1, characteristic.getUuid().toString(), + characteristic.getProperties(), characteristic.getValue()); + } else { + if (isServiceDiscoveryRun) { + Log.w(TAG, "onCharacteristicRead during discovery error: " + status); + + Log.d(TAG, "Non-readable characteristic " + characteristic.getUuid() + + " for service " + characteristic.getService().getUuid()); + 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); } + } - synchronized (readWriteQueue) { - ioJobPending = false; - } + if (isServiceDiscoveryRun) { - 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); - } - } + // last entry of pending service discovery run -> send discovery finished state update + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == foundHandle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + } - performNextIO(); - return; + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; } - 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); + performNextIO(); } public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt, @@ -249,6 +302,15 @@ public class QtBluetoothLE { return; } + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(handle, IoJobType.Write), -1); + if (requestTimedOut) { + Log.w(TAG, "Late char write reply after timeout was hit for handle " + handle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } + int errorCode; //This must be in sync with QLowEnergyService::ServiceError switch (status) { @@ -281,81 +343,84 @@ public class QtBluetoothLE { 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; - } - + int foundHandle = -1; synchronized (this) { - if (uuidToEntry.isEmpty()) // ignore data if internal setup is not ready; + foundHandle = handleForDescriptor(descriptor); + if (foundHandle == -1 || foundHandle >= entries.size() ) { + Log.w(TAG, "Cannot find descriptor read request for read notification - handle: " + + foundHandle + " size: " + entries.size()); + + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } + performNextIO(); return; + } } + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(foundHandle, IoJobType.Read), -1); + if (requestTimedOut) { + Log.w(TAG, "Late descriptor read reply after timeout was hit for handle " + + foundHandle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } - if (!isServiceDiscoveryRun) { + GattEntry entry = entries.get(foundHandle); + final boolean isServiceDiscoveryRun = !entry.valueKnown; + entry.valueKnown = true; - int foundHandle = -1; - synchronized (this) { - foundHandle = handleForDescriptor(descriptor); + if (status == BluetoothGatt.GATT_SUCCESS) { + //TODO avoid sending service and characteristic uuid -> handles should be sufficient + leDescriptorRead(qtObject, descriptor.getCharacteristic().getService().getUuid().toString(), + descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1, + descriptor.getUuid().toString(), descriptor.getValue()); + } else { + if (isServiceDiscoveryRun) { + //ignore + Log.w(TAG, "onDescriptorcRead during discovery error: " + status); + } else { + // This must be in sync with QLowEnergyService::DescriptorReadError + final int descriptorReadError = 6; + leServiceError(qtObject, foundHandle + 1, descriptorReadError); } - synchronized (readWriteQueue) { - ioJobPending = false; + } + + if (isServiceDiscoveryRun) { + // last entry of pending service discovery run? ->send discovery finished state update + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == foundHandle) { + finishCurrentServiceDiscovery(entry.associatedServiceHandle); } - 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); + /* 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) { + byte[] bytearray = descriptor.getValue(); + final int value = (bytearray != null && bytearray.length > 0) ? bytearray[0] : 0; + // notification or indication bit set? + if ((value & 0x03) > 0) { + Log.d(TAG, "Found descriptor with automatic notifications."); + mBluetoothGatt.setCharacteristicNotification( + descriptor.getCharacteristic(), true); } } - - 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); - } + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; } - performServiceDetailDiscoveryForHandle(runningHandle + 1, false); + performNextIO(); } public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt, @@ -367,6 +432,16 @@ public class QtBluetoothLE { int handle = handleForDescriptor(descriptor); + boolean requestTimedOut = !handleForTimeout.compareAndSet( + modifiedReadWriteHandle(handle, IoJobType.Write), -1); + if (requestTimedOut) { + Log.w(TAG, "Late descriptor write reply after timeout was hit for handle " + + handle); + // Timeout has hit before this response -> ignore the response + // no need to unlock ioJobPending -> the timeout has done that already + return; + } + int errorCode; //This must be in sync with QLowEnergyService::ServiceError switch (status) { @@ -407,7 +482,31 @@ public class QtBluetoothLE { return false; } - mBluetoothGatt = mRemoteGattDevice.connectGatt(qtactivity, false, gattCallback); + try { + // BluetoothDevice.,connectGatt(Context, boolean, BluetoothGattCallback, int) was + // officially introduced by Android API v23. Earlier Android versions have a private + // implementation already though. Let's check at runtime and use it if possible. + // + // In general the new connectGatt() seems to be much more reliable than the function + // that doesn't specify the transport layer. + + Class[] args = new Class[4]; + args[0] = android.content.Context.class; + args[1] = boolean.class; + args[2] = android.bluetooth.BluetoothGattCallback.class; + args[3] = int.class; + Method connectMethod = mRemoteGattDevice.getClass().getDeclaredMethod("connectGatt", args); + if (connectMethod != null) { + mBluetoothGatt = (BluetoothGatt) connectMethod.invoke(mRemoteGattDevice, qtContext, + false, gattCallback, + 2 /*TRANSPORT_LE*/); + Log.w(TAG, "Using Android v23 BluetoothDevice.connectGatt()"); + } + } catch (Exception ex) { + // fallback to less reliable API 18 version + mBluetoothGatt = mRemoteGattDevice.connectGatt(qtContext, false, gattCallback); + } + return mBluetoothGatt != null; } @@ -434,7 +533,15 @@ public class QtBluetoothLE { public BluetoothGattService service = null; public BluetoothGattCharacteristic characteristic = null; public BluetoothGattDescriptor descriptor = null; - public int endHandle; + /* + * endHandle defined for GattEntryType.Service and GattEntryType.CharacteristicValue + * If the type is service this is the value of the last Gatt entry belonging to the very + * same service. If the type is a char value it is the entries index inside + * the "entries" list. + */ + public int endHandle = -1; + // pointer back to the handle that describes the service that this GATT entry belongs to + public int associatedServiceHandle; } private enum IoJobType @@ -450,8 +557,12 @@ public class QtBluetoothLE { public IoJobType jobType; } + // service uuid -> service handle mapping (there can be more than one service with same uuid) private final Hashtable<UUID, List<Integer>> uuidToEntry = new Hashtable<UUID, List<Integer>>(100); + // index into array is equivalent to handle id private final ArrayList<GattEntry> entries = new ArrayList<GattEntry>(100); + //backlog of to be discovered services + // TODO remove private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>(); @@ -551,10 +662,12 @@ public class QtBluetoothLE { GattEntry serviceEntry = new GattEntry(); serviceEntry.type = GattEntryType.Service; serviceEntry.service = service; - entries.add(entry); + entries.add(serviceEntry); // remember handle for the service for later update int serviceHandle = entries.size() - 1; + //point to itself -> mostly done for consistence reasons with other entries + serviceEntry.associatedServiceHandle = serviceHandle; //some devices may have more than one service with the same uuid List<Integer> old = uuidToEntry.get(service.getUuid()); @@ -569,11 +682,15 @@ public class QtBluetoothLE { entry = new GattEntry(); entry.type = GattEntryType.Characteristic; entry.characteristic = characteristic; + entry.associatedServiceHandle = serviceHandle; + //entry.endHandle = .. undefined entries.add(entry); // this emulates GATT value attributes entry = new GattEntry(); entry.type = GattEntryType.CharacteristicValue; + entry.associatedServiceHandle = serviceHandle; + entry.endHandle = entries.size(); // special case -> current index in entries list entries.add(entry); // add all descriptors @@ -582,30 +699,31 @@ public class QtBluetoothLE { entry = new GattEntry(); entry.type = GattEntryType.Descriptor; entry.descriptor = desc; + entry.associatedServiceHandle = serviceHandle; + //entry.endHandle = .. undefined 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(); } + + // kill all timeout handlers + timeoutHandler.removeCallbacksAndMessages(null); + handleForTimeout.set(-1); + synchronized (readWriteQueue) { readWriteQueue.clear(); } @@ -648,28 +766,15 @@ public class QtBluetoothLE { 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"); - } + // current service already discovered or under investigation + if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) { + Log.w(TAG, "Service already known or to be discovered"); return true; } - if (!entry.valueKnown) { - performServiceDetailDiscoveryForHandle(serviceHandle, true); - } else { - Log.w(TAG, "Service already discovered"); - } + servicesToBeDiscovered.add(serviceHandle); + scheduleServiceDetailDiscovery(serviceHandle); + performNextIO(); } catch (Exception ex) { ex.printStackTrace(); @@ -710,98 +815,76 @@ public class QtBluetoothLE { return builder.toString(); } - private void finishCurrentServiceDiscovery() + //TODO function not yet used + private void finishCurrentServiceDiscovery(int handleDiscoveredService) { - int currentEntry = currentServiceInDiscovery; - GattEntry discoveredService = entries.get(currentServiceInDiscovery); + Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService); + GattEntry discoveredService = entries.get(handleDiscoveredService); 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()) { + synchronized (this) { try { - int nextService = servicesToBeDiscovered.remove(); - performServiceDetailDiscoveryForHandle(nextService, true); - } catch (IndexOutOfBoundsException ex) { + servicesToBeDiscovered.removeFirst(); + } catch (NoSuchElementException ex) { Log.w(TAG, "Expected queued service but didn't find any"); } } + + leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(), + handleDiscoveredService + 1, discoveredService.endHandle + 1); } - private synchronized void performServiceDetailDiscoveryForHandle(int nextHandle, boolean searchStarted) + /* + Internal Helper function for discoverServiceDetails() + + Adds all Gatt entries for the given service to the readWriteQueue to be discovered. + This function only ever adds read requests to the queue. + + //TODO function not yet used + */ + private void scheduleServiceDetailDiscovery(int serviceHandle) { - try { - if (searchStarted) { - currentServiceInDiscovery = nextHandle; - runningHandle = ++nextHandle; - } else { - runningHandle = nextHandle; - } + GattEntry serviceEntry = entries.get(serviceHandle); + final int endHandle = serviceEntry.endHandle; - GattEntry entry; - try { - entry = entries.get(nextHandle); - } catch (IndexOutOfBoundsException ex) { - //ex.printStackTrace(); - Log.w(TAG, "Last entry of last service read"); - finishCurrentServiceDiscovery(); - return; - } + if (serviceHandle == endHandle) { + Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover"); + finishCurrentServiceDiscovery(serviceHandle); + 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; - } + synchronized (readWriteQueue) { + // entire block inside mutex to ensure all service discovery jobs go in one after the other + // ensures that serviceDiscovered() signal is sent when required - } catch(Exception ex) { - ex.printStackTrace(); + + // serviceHandle + 1 -> ignore service handle itself + for (int i = serviceHandle + 1; i <= endHandle; i++) { + GattEntry entry = entries.get(i); + + switch (entry.type) { + case Characteristic: + case Descriptor: + // we schedule CharacteristicValue for initial discovery to simplify + // detection of the end of service discovery process + // performNextIO() ignores CharacteristicValue GATT entries + case CharacteristicValue: + break; + case Service: + // should not really happen unless endHandle is wrong + Log.w(TAG, "scheduleServiceDetailDiscovery: wrong endHandle"); + return; + } + + // only descriptor and characteristic fall through to this point + ReadWriteJob newJob = new ReadWriteJob(); + newJob.entry = entry; + newJob.jobType = IoJobType.Read; + + final boolean result = readWriteQueue.add(newJob); + if (!result) + Log.w(TAG, "Cannot add service discovery job for " + serviceEntry.service.getUuid() + + " on item " + entry.type); + } } } @@ -829,11 +912,13 @@ public class QtBluetoothLE { 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; + case 2: //WriteSigned + newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_SIGNED; + break; default: newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT; break; @@ -956,6 +1041,30 @@ public class QtBluetoothLE { return true; } + // Called by TimeoutRunnable if the current I/O job timeoutd out. + // By the time we reach this point the handleForTimeout counter has already been reset + // and the regular responses will be blocked off. + private void interruptCurrentIO(int handle) + { + //unlock the queue for next item + synchronized (readWriteQueue) { + ioJobPending = false; + } + + performNextIO(); + + GattEntry entry = entries.get(handle); + if (entry == null) + return; + if (entry.valueKnown) + return; + entry.valueKnown = true; + + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry != null && serviceEntry.endHandle == handle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + } + /* The queuing is required because two writeCharacteristic/writeDescriptor calls cannot execute at the same time. The second write must happen after the @@ -968,50 +1077,111 @@ public class QtBluetoothLE { boolean skip = false; final ReadWriteJob nextJob; + int handle = -1; + synchronized (readWriteQueue) { if (readWriteQueue.isEmpty() || ioJobPending) return; nextJob = readWriteQueue.remove(); + switch (nextJob.entry.type) { + case Characteristic: + handle = handleForCharacteristic(nextJob.entry.characteristic); + break; + case Descriptor: + handle = handleForDescriptor(nextJob.entry.descriptor); + break; + case CharacteristicValue: + handle = nextJob.entry.endHandle; + default: + break; + } + + // timeout handler and handleForTimeout atomic must be setup before + // executing the request. Sometimes the callback is quicker than executing the + // remainder of this function. Therefore enable the atomic early such that + // callback handlers start hanging in the readWriteQueue sync block which + // we are still occupying here. + timeoutHandler.removeCallbacksAndMessages(null); // remove any timeout handlers + handleForTimeout.set(modifiedReadWriteHandle(handle, nextJob.jobType)); - Log.w(TAG, "Performing queued job " + nextJob.jobType); if (nextJob.jobType == IoJobType.Read) skip = executeReadJob(nextJob); else skip = executeWriteJob(nextJob); - if (!skip) + if (skip) { + handleForTimeout.set(-1); // not a pending call -> release atomic + } else { ioJobPending = true; + timeoutHandler.postDelayed(new TimeoutRunnable( + modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT); + } + + Log.w(TAG, "Performing queued job, handle: " + handle + " " + nextJob.jobType + " (" + + (nextJob.requestedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) + + ") ValueKnown: " + nextJob.entry.valueKnown + " Skipping: " + skip + + " " + nextJob.entry.type); } - if (skip) { - Log.w(TAG, "Skipping: " + nextJob.entry.type); + GattEntry entry = nextJob.entry; + if (skip) { /* 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 + we have to report an error back to Qt. The error report 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) { + // during service discovery we do not report error but emit characteristicRead() + // any other time a failure emits serviceError() signal + + final boolean isServiceDiscovery = !entry.valueKnown; - if (handle != -1) { + if (isServiceDiscovery) { + entry.valueKnown = true; + switch (entry.type) { + case Characteristic: + Log.d(TAG, "Non-readable characteristic " + entry.characteristic.getUuid() + + " for service " + entry.characteristic.getService().getUuid()); + leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(), + handle + 1, entry.characteristic.getUuid().toString(), + entry.characteristic.getProperties(), entry.characteristic.getValue()); + break; + case Descriptor: + // 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(), + handle + 1, entry.descriptor.getUuid().toString(), + entry.descriptor.getValue()); + break; + case CharacteristicValue: + // for more details see scheduleServiceDetailDiscovery(int) + // ignore and continue unless last entry + GattEntry serviceEntry = entries.get(entry.associatedServiceHandle); + if (serviceEntry.endHandle == handle) + finishCurrentServiceDiscovery(entry.associatedServiceHandle); + break; + case Service: + Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen."); + break; + } + } else { 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 + errorCode = (entry.type == GattEntryType.Characteristic) ? + 5 : 6; // CharacteristicReadError : DescriptorReadError } else { - errorCode = (nextJob.entry.type == GattEntryType.Characteristic) ? - 2 : 3; // CharacteristicWriteError : DescriptorWriteError + errorCode = (entry.type == GattEntryType.Characteristic) ? + 2 : 3; // CharacteristicWriteError : DescriptorWriteError } leServiceError(qtObject, handle + 1, errorCode); @@ -1091,22 +1261,100 @@ public class QtBluetoothLE { boolean result; switch (nextJob.entry.type) { case Characteristic: - result = mBluetoothGatt.readCharacteristic(nextJob.entry.characteristic); + try { + result = mBluetoothGatt.readCharacteristic(nextJob.entry.characteristic); + } catch (java.lang.SecurityException se) { + // QTBUG-59917 -> HID services cause problems since Android 5.1 + se.printStackTrace(); + result = false; + } if (!result) return true; // skip break; case Descriptor: - result = mBluetoothGatt.readDescriptor(nextJob.entry.descriptor); + try { + result = mBluetoothGatt.readDescriptor(nextJob.entry.descriptor); + } catch (java.lang.SecurityException se) { + // QTBUG-59917 -> HID services cause problems since Android 5.1 + se.printStackTrace(); + result = false; + } if (!result) return true; // skip break; case Service: - case CharacteristicValue: return true; + case CharacteristicValue: + return true; //skip } return false; } + /* + * Modifies and returns the given \a handle such that the job + * \a type is encoded into the returned handle. Hereby we take advantage of the fact that + * a Bluetooth Low Energy handle is only 16 bit. The handle will be the bottom two bytes + * and the job type will be in the top 2 bytes. + * + * top 2 bytes + * - 0x01 -> Read Job + * - 0x02 -> Write Job + * + * This is done in connection with handleForTimeout and assists in the process of + * detecting accidental interruption by the timeout handler. + * If two requests for the same handle are scheduled behind each other there is the + * theoretical chance that the first request comes back normally while the second request + * is interrupted by the timeout handler. This risk still exists but this function ensures that + * at least back to back requests of differing types cannot affect each other via the timeout + * handler. + */ + private int modifiedReadWriteHandle(int handle, IoJobType type) + { + int modifiedHandle = handle; + // ensure we have 16bit handle only + if (handle > 0xFFFF) + Log.w(TAG, "Invalid handle"); + + modifiedHandle = (modifiedHandle & 0xFFFF); + + switch (type) { + case Write: + modifiedHandle = (modifiedHandle | 0x00010000); + break; + case Read: + modifiedHandle = (modifiedHandle | 0x00020000); + break; + } + + return modifiedHandle; + } + + // Directly called from public Qt API + public boolean requestConnectionUpdatePriority(double minimalInterval) + { + if (mBluetoothGatt == null) + return false; + + try { + //Android API v21 + Method connectionUpdateMethod = mBluetoothGatt.getClass().getDeclaredMethod( + "requestConnectionPriority", int.class); + if (connectionUpdateMethod == null) + return false; + + int requestPriority = 0; // BluetoothGatt.CONNECTION_PRIORITY_BALANCED + if (minimalInterval < 30) + requestPriority = 1; // BluetoothGatt.CONNECTION_PRIORITY_HIGH + else if (minimalInterval > 100) + requestPriority = 2; //BluetoothGatt/CONNECTION_PRIORITY_LOW_POWER + + Object result = connectionUpdateMethod.invoke(mBluetoothGatt, requestPriority); + return (Boolean) result; + } catch (Exception ex) { + 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, |