summaryrefslogtreecommitdiffstats
path: root/src/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/android')
-rw-r--r--src/android/CMakeLists.txt9
-rw-r--r--src/android/android.pro3
-rw-r--r--src/android/bluetooth/AndroidManifest.xml2
-rw-r--r--src/android/bluetooth/CMakeLists.txt27
-rw-r--r--src/android/bluetooth/bluetooth.pro19
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothBroadcastReceiver.java188
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattCharacteristic.java44
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattDescriptor.java39
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothInputStreamThread.java69
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLE.java1849
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java989
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothSocketServer.java (renamed from src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothSocketServer.java)99
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothBroadcastReceiver.java193
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothInputStreamThread.java104
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java1532
-rw-r--r--src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java611
-rw-r--r--src/android/nfc/AndroidManifest.xml2
-rw-r--r--src/android/nfc/CMakeLists.txt22
-rw-r--r--src/android/nfc/nfc.pro16
-rw-r--r--src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfc.java (renamed from src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfc.java)115
-rw-r--r--src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfcBroadcastReceiver.java38
-rw-r--r--src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfcBroadcastReceiver.java72
22 files changed, 3359 insertions, 2683 deletions
diff --git a/src/android/CMakeLists.txt b/src/android/CMakeLists.txt
new file mode 100644
index 00000000..a8b61270
--- /dev/null
+++ b/src/android/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(TARGET Qt::Bluetooth)
+ add_subdirectory(bluetooth)
+endif()
+if(TARGET Qt::Nfc)
+ add_subdirectory(nfc)
+endif()
diff --git a/src/android/android.pro b/src/android/android.pro
deleted file mode 100644
index f8f5c05e..00000000
--- a/src/android/android.pro
+++ /dev/null
@@ -1,3 +0,0 @@
-TEMPLATE = subdirs
-qtHaveModule(bluetooth): SUBDIRS += bluetooth
-qtHaveModule(nfc): SUBDIRS += nfc
diff --git a/src/android/bluetooth/AndroidManifest.xml b/src/android/bluetooth/AndroidManifest.xml
index 9148e171..27355299 100644
--- a/src/android/bluetooth/AndroidManifest.xml
+++ b/src/android/bluetooth/AndroidManifest.xml
@@ -2,6 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
- package="org.qtproject.qt5.android.bluetooth">
+ package="org.qtproject.qt.android.bluetooth">
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
</manifest>
diff --git a/src/android/bluetooth/CMakeLists.txt b/src/android/bluetooth/CMakeLists.txt
new file mode 100644
index 00000000..f8692576
--- /dev/null
+++ b/src/android/bluetooth/CMakeLists.txt
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+set(java_sources
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothBroadcastReceiver.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothInputStreamThread.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothLE.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothSocketServer.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothGattCharacteristic.java
+ src/org/qtproject/qt/android/bluetooth/QtBluetoothGattDescriptor.java
+)
+
+qt_internal_add_jar(Qt${QtConnectivity_VERSION_MAJOR}AndroidBluetooth
+ INCLUDE_JARS ${QT_ANDROID_JAR}
+ SOURCES ${java_sources}
+ OUTPUT_DIR "${QT_BUILD_DIR}/jar"
+)
+
+qt_path_join(destination ${INSTALL_DATADIR} "jar")
+
+install_jar(Qt${QtConnectivity_VERSION_MAJOR}AndroidBluetooth
+ DESTINATION ${destination}
+ COMPONENT Devel
+)
+
+add_dependencies(Bluetooth Qt${QtConnectivity_VERSION_MAJOR}AndroidBluetooth)
diff --git a/src/android/bluetooth/bluetooth.pro b/src/android/bluetooth/bluetooth.pro
deleted file mode 100644
index b76b392c..00000000
--- a/src/android/bluetooth/bluetooth.pro
+++ /dev/null
@@ -1,19 +0,0 @@
-TARGET = QtAndroidBluetooth
-
-CONFIG += java
-DESTDIR = $$[QT_INSTALL_PREFIX/get]/jar
-API_VERSION = android-21
-
-PATHPREFIX = $$PWD/src/org/qtproject/qt5/android/bluetooth
-
-JAVACLASSPATH += $$PWD/src/
-JAVASOURCES += \
- $$PATHPREFIX/QtBluetoothBroadcastReceiver.java \
- $$PATHPREFIX/QtBluetoothSocketServer.java \
- $$PATHPREFIX/QtBluetoothInputStreamThread.java \
- $$PATHPREFIX/QtBluetoothLE.java \
- $$PATHPREFIX/QtBluetoothLEServer.java
-
-# install
-target.path = $$[QT_INSTALL_PREFIX]/jar
-INSTALLS += target
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothBroadcastReceiver.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothBroadcastReceiver.java
new file mode 100644
index 00000000..5df5cb81
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothBroadcastReceiver.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2016 Lauri Laanmets (Proekspert AS) <lauri.laanmets@eesti.ee>
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.List;
+
+public class QtBluetoothBroadcastReceiver extends BroadcastReceiver
+{
+ /* Pointer to the Qt object that "owns" the Java object */
+ @SuppressWarnings("WeakerAccess")
+ long qtObject = 0;
+ @SuppressWarnings("WeakerAccess")
+ static Context qtContext = null;
+
+ // These are opaque tokens that could be used to match the completed action
+ private static final int TURN_BT_ENABLED = 3330;
+ private static final int TURN_BT_DISCOVERABLE = 3331;
+ private static final int TURN_BT_DISABLED = 3332;
+
+ // The 'Disable' action identifier is hidden in the public APIs so we define it here
+ static final String ACTION_REQUEST_DISABLE =
+ "android.bluetooth.adapter.action.REQUEST_DISABLE";
+
+ private static final String TAG = "QtBluetoothBroadcastReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent)
+ {
+ synchronized (qtContext) {
+ if (qtObject == 0)
+ return;
+
+ jniOnReceive(qtObject, context, intent);
+ }
+ }
+
+ void unregisterReceiver()
+ {
+ synchronized (qtContext) {
+ qtObject = 0;
+ try {
+ qtContext.unregisterReceiver(this);
+ } catch (Exception ex) {
+ Log.d(TAG, "Trying to unregister a BroadcastReceiver which is not yet registered");
+ }
+ }
+ }
+
+ native void jniOnReceive(long qtObject, Context context, Intent intent);
+
+ public static void setContext(Context context)
+ {
+ qtContext = context;
+ }
+
+ static boolean setDisabled()
+ {
+ if (!(qtContext instanceof android.app.Activity)) {
+ Log.w(TAG, "Bluetooth cannot be disabled from a service.");
+ return false;
+ }
+ // The 'disable' is hidden in the public API and as such
+ // there are no availability guarantees; may throw an "ActivityNotFoundException"
+ Intent intent = new Intent(ACTION_REQUEST_DISABLE);
+
+ try {
+ ((Activity)qtContext).startActivityForResult(intent, TURN_BT_DISABLED);
+ } catch (Exception ex) {
+ Log.w(TAG, "setDisabled() failed to initiate Bluetooth disablement");
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ static boolean setDiscoverable()
+ {
+ if (!(qtContext instanceof android.app.Activity)) {
+ Log.w(TAG, "Discovery mode cannot be enabled from a service.");
+ return false;
+ }
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
+ try {
+ ((Activity)qtContext).startActivityForResult(intent, TURN_BT_DISCOVERABLE);
+ } catch (Exception ex) {
+ Log.w(TAG, "setDiscoverable() failed to initiate Bluetooth discoverability change");
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ static boolean setEnabled()
+ {
+ if (!(qtContext instanceof android.app.Activity)) {
+ Log.w(TAG, "Bluetooth cannot be enabled from a service.");
+ return false;
+ }
+
+ Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ try {
+ ((Activity)qtContext).startActivityForResult(intent, TURN_BT_ENABLED);
+ } catch (Exception ex) {
+ Log.w(TAG, "setEnabled() failed to initiate Bluetooth enablement");
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ static boolean setPairingMode(String address, boolean isPairing)
+ {
+ BluetoothManager manager =
+ (BluetoothManager)qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+ if (manager == null)
+ return false;
+
+ BluetoothAdapter adapter = manager.getAdapter();
+ if (adapter == null)
+ return false;
+
+ // Uses reflection as the removeBond() is not part of public API
+ try {
+ BluetoothDevice device = adapter.getRemoteDevice(address);
+ String methodName = "createBond";
+ if (!isPairing)
+ methodName = "removeBond";
+
+ Method m = device.getClass()
+ .getMethod(methodName, (Class[]) null);
+ m.invoke(device, (Object[]) null);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ * Returns a list of remote devices confirmed to be connected.
+ *
+ * This list is not complete as it only detects GATT/BtLE related connections.
+ * Unfortunately there is no API that provides the complete list.
+ *
+ */
+ static String[] getConnectedDevices()
+ {
+ BluetoothManager bluetoothManager =
+ (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+
+ if (bluetoothManager == null) {
+ Log.w(TAG, "Failed to retrieve connected devices");
+ return new String[0];
+ }
+
+ List<BluetoothDevice> gattConnections =
+ bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
+ List<BluetoothDevice> gattServerConnections =
+ bluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
+
+ // Process found remote connections but avoid duplications
+ HashSet<String> set = new HashSet<String>();
+ for (Object gattConnection : gattConnections)
+ set.add(gattConnection.toString());
+
+ for (Object gattServerConnection : gattServerConnections)
+ set.add(gattServerConnection.toString());
+
+ return set.toArray(new String[set.size()]);
+ }
+}
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattCharacteristic.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattCharacteristic.java
new file mode 100644
index 00000000..6473541a
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattCharacteristic.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.os.Build;
+
+import java.util.UUID;
+
+class QtBluetoothGattCharacteristic extends BluetoothGattCharacteristic {
+ QtBluetoothGattCharacteristic(UUID uuid, int properties, int permissions,
+ int minimumValueLength, int maximumValueLength) {
+ super(uuid, properties, permissions);
+ minValueLength = minimumValueLength;
+ maxValueLength = maximumValueLength;
+ }
+ int minValueLength;
+ int maxValueLength;
+ // Starting from API 33 Android Bluetooth deprecates characteristic local value caching by
+ // deprecating the getValue() and setValue() accessors. For peripheral role we store the value
+ // locally in the characteristic as a convenience - looking up the value on the C++ side would
+ // be somewhat complicated. This should be safe as all accesses to this class are synchronized.
+ // For clarity: For API levels below 33 we still need to use the setValue() of the base class
+ // because Android internally uses getValue() with APIs below 33.
+ boolean setLocalValue(byte[] value) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ m_localValue = value;
+ return true;
+ } else {
+ return setValue(value);
+ }
+ }
+
+ byte[] getLocalValue()
+ {
+ if (Build.VERSION.SDK_INT >= 33)
+ return m_localValue;
+ else
+ return getValue();
+ }
+
+ private byte[] m_localValue = null;
+}
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattDescriptor.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattDescriptor.java
new file mode 100644
index 00000000..b6c195d3
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothGattDescriptor.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+import android.bluetooth.BluetoothGattDescriptor;
+import android.os.Build;
+
+import java.util.UUID;
+
+class QtBluetoothGattDescriptor extends BluetoothGattDescriptor {
+ QtBluetoothGattDescriptor(UUID uuid, int permissions) {
+ super(uuid, permissions);
+ }
+ // Starting from API 33 Android Bluetooth deprecates descriptor local value caching by
+ // deprecating the getValue() and setValue() accessors. For peripheral role we store the value
+ // locally in the descriptor as a convenience - looking up the value on the C++ side would
+ // be somewhat complicated. This should be safe as all accesses to this class are synchronized.
+ // For clarity: For API levels below 33 we still need to use the setValue() of the base class
+ // because Android internally uses getValue() with APIs below 33.
+ boolean setLocalValue(byte[] value) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ m_localValue = value;
+ return true;
+ } else {
+ return setValue(value);
+ }
+ }
+
+ byte[] getLocalValue()
+ {
+ if (Build.VERSION.SDK_INT >= 33)
+ return m_localValue;
+ else
+ return getValue();
+ }
+
+ private byte[] m_localValue = null;
+}
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothInputStreamThread.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothInputStreamThread.java
new file mode 100644
index 00000000..3c12cb34
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothInputStreamThread.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+import java.io.InputStream;
+import java.io.IOException;
+import android.util.Log;
+
+@SuppressWarnings("WeakerAccess")
+class QtBluetoothInputStreamThread extends Thread
+{
+ /* Pointer to the Qt object that "owns" the Java object */
+ @SuppressWarnings("CanBeFinal")
+ long qtObject = 0;
+ @SuppressWarnings("CanBeFinal")
+ boolean logEnabled = false;
+ private static final String TAG = "QtBluetooth";
+ private InputStream m_inputStream = null;
+
+ //error codes
+ static final int QT_MISSING_INPUT_STREAM = 0;
+ static final int QT_READ_FAILED = 1;
+ static final int QT_THREAD_INTERRUPTED = 2;
+
+ QtBluetoothInputStreamThread()
+ {
+ setName("QtBtInputStreamThread");
+ }
+
+ void setInputStream(InputStream stream)
+ {
+ m_inputStream = stream;
+ }
+
+ @Override
+ public void run()
+ {
+ if (m_inputStream == null) {
+ errorOccurred(qtObject, QT_MISSING_INPUT_STREAM);
+ return;
+ }
+
+ byte[] buffer = new byte[1000];
+ int bytesRead;
+
+ try {
+ while (!isInterrupted()) {
+ //this blocks until we see incoming data
+ //or close() on related BluetoothSocket is called
+ bytesRead = m_inputStream.read(buffer);
+ readyData(qtObject, buffer, bytesRead);
+ }
+
+ errorOccurred(qtObject, QT_THREAD_INTERRUPTED);
+ } catch (IOException ex) {
+ if (logEnabled)
+ Log.d(TAG, "InputStream.read() failed:" + ex.toString());
+ ex.printStackTrace();
+ errorOccurred(qtObject, QT_READ_FAILED);
+ }
+
+ if (logEnabled)
+ Log.d(TAG, "Leaving input stream thread");
+ }
+
+ static native void errorOccurred(long qtObject, int errorCode);
+ static native void readyData(long qtObject, byte[] buffer, int bufferLength);
+}
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLE.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLE.java
new file mode 100644
index 00000000..d133d8dc
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLE.java
@@ -0,0 +1,1849 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+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.bluetooth.BluetoothManager;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+
+
+class QtBluetoothLE {
+ private static final String TAG = "QtBluetoothGatt";
+ private BluetoothAdapter mBluetoothAdapter = null;
+ private boolean mLeScanRunning = false;
+
+ private BluetoothGatt mBluetoothGatt = null;
+ private HandlerThread mHandlerThread = null;
+ private Handler mHandler = null;
+ private Constructor mCharacteristicConstructor = null;
+ private String mRemoteGattAddress;
+ private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+ private final int MAX_MTU = 512;
+ private final int DEFAULT_MTU = 23;
+ private int mSupportedMtu = -1;
+
+ /*
+ * 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.
+ */
+ // handle values above zero are for regular handle specific read/write requests
+ // handle values below zero are reserved for handle-independent requests
+ private int HANDLE_FOR_RESET = -1;
+ private int HANDLE_FOR_MTU_EXCHANGE = -2;
+ private int HANDLE_FOR_RSSI_READ = -3;
+ private AtomicInteger handleForTimeout = new AtomicInteger(HANDLE_FOR_RESET); // implies not running by default
+
+ private final int RUNNABLE_TIMEOUT = 3000; // 3 seconds
+ private final Handler timeoutHandler = new Handler(Looper.getMainLooper());
+
+ private BluetoothLeScanner mBluetoothLeScanner = null;
+
+ private class TimeoutRunnable implements Runnable {
+ TimeoutRunnable(int handle) { pendingJobHandle = handle; }
+ @Override
+ public void run() {
+ boolean timeoutStillValid = handleForTimeout.compareAndSet(pendingJobHandle, HANDLE_FOR_RESET);
+ if (timeoutStillValid) {
+ Log.w(TAG, "****** Timeout for request on handle " + (pendingJobHandle & 0xffff));
+ Log.w(TAG, "****** Looks like the peripheral does NOT act in " +
+ "accordance to Bluetooth 4.x spec.");
+ Log.w(TAG, "****** Please check server implementation. Continuing under " +
+ "reservation.");
+
+ if (pendingJobHandle > HANDLE_FOR_RESET)
+ interruptCurrentIO(pendingJobHandle & 0xffff);
+ else if (pendingJobHandle < HANDLE_FOR_RESET)
+ interruptCurrentIO(pendingJobHandle);
+ }
+ }
+
+ // contains handle (0xffff) and top 2 byte contain the job type (0xffff0000)
+ private int pendingJobHandle = -1;
+ };
+
+ // The handleOn* functions in this class are callback handlers which are synchronized
+ // to "this" client object. This protects the member variables which could be
+ // concurrently accessed from Qt (JNI) thread and different Java threads *)
+ // *) The newer Android API (starting Android 8.1) synchronizes callbacks to one
+ // Java thread, but this is not true for the earlier API which we still support.
+ //
+ // In case bond state has been changed due to access to a restricted handle,
+ // Android never completes the operation which triggered the devices to bind
+ // and thus never fires on(Characteristic|Descriptor)(Read|Write) callback,
+ // causing TimeoutRunnable to interrupt pending job,
+ // albeit the read/write job hasn't been actually executed by the peripheral;
+ // re-add the currently pending job to the queue's head and re-run it.
+ // If, by some reason, bonding process has been interrupted, either
+ // re-add the currently pending job to the queue's head and re-run it.
+ private synchronized void handleOnReceive(Context context, Intent intent)
+ {
+ if (mBluetoothGatt == null)
+ return;
+
+ final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device == null || !device.getAddress().equals(mBluetoothGatt.getDevice().getAddress()))
+ return;
+
+ final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+ final int previousBondState =
+ intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);
+
+ if (bondState == BluetoothDevice.BOND_BONDING) {
+ if (pendingJob == null
+ || pendingJob.jobType == IoJobType.Mtu || pendingJob.jobType == IoJobType.Rssi) {
+ return;
+ }
+
+ timeoutHandler.removeCallbacksAndMessages(null);
+ handleForTimeout.set(HANDLE_FOR_RESET);
+ } else if (previousBondState == BluetoothDevice.BOND_BONDING &&
+ (bondState == BluetoothDevice.BOND_BONDED || bondState == BluetoothDevice.BOND_NONE)) {
+ if (pendingJob == null
+ || pendingJob.jobType == IoJobType.Mtu || pendingJob.jobType == IoJobType.Rssi) {
+ return;
+ }
+
+ readWriteQueue.addFirst(pendingJob);
+ pendingJob = null;
+
+ performNextIO();
+ } else if (previousBondState == BluetoothDevice.BOND_BONDED
+ && bondState == BluetoothDevice.BOND_NONE) {
+ // peripheral or central removed the bond information;
+ // if it was peripheral, the connection attempt would fail with PIN_OR_KEY_MISSING,
+ // which is handled by Android by broadcasting ACTION_BOND_STATE_CHANGED
+ // with new state BOND_NONE, without actually deleting the bond information :facepalm:
+ // if we get there, it is safer to delete it now, by invoking the undocumented API call
+ try {
+ device.getClass().getMethod("removeBond").invoke(device);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+
+ private class BondStateBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ handleOnReceive(context, intent);
+ }
+ };
+ private BroadcastReceiver bondStateBroadcastReceiver = null;
+
+ /* Pointer to the Qt object that "owns" the Java object */
+ @SuppressWarnings({"CanBeFinal", "WeakerAccess"})
+ long qtObject = 0;
+ @SuppressWarnings("WeakerAccess")
+ Context qtContext = null;
+
+ @SuppressWarnings("WeakerAccess")
+ QtBluetoothLE(Context context) {
+ qtContext = context;
+
+ BluetoothManager manager =
+ (BluetoothManager)qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+ if (manager == null)
+ return;
+
+ mBluetoothAdapter = manager.getAdapter();
+ if (mBluetoothAdapter == null)
+ return;
+
+ mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
+ }
+
+ QtBluetoothLE(final String remoteAddress, Context context) {
+ this(context);
+ mRemoteGattAddress = remoteAddress;
+ }
+
+ /*************************************************************/
+ /* Device scan */
+ /* Returns true, if request was successfully completed */
+ /* This function is called from Qt thread, but only accesses */
+ /* variables that are not accessed from Java threads */
+ /*************************************************************/
+
+ boolean scanForLeDevice(final boolean isEnabled) {
+ if (isEnabled == mLeScanRunning)
+ return true;
+
+ if (mBluetoothLeScanner == null) {
+ Log.w(TAG, "Cannot start LE scan, no bluetooth scanner");
+ return false;
+ }
+
+ if (isEnabled) {
+ Log.d(TAG, "Attempting to start BTLE scan");
+ ScanSettings.Builder settingsBuilder = new ScanSettings.Builder();
+ settingsBuilder = settingsBuilder.setScanMode(ScanSettings.SCAN_MODE_BALANCED);
+ ScanSettings settings = settingsBuilder.build();
+
+ List<ScanFilter> filterList = new ArrayList<ScanFilter>();
+
+ mBluetoothLeScanner.startScan(filterList, settings, leScanCallback);
+ mLeScanRunning = true;
+ } else {
+ Log.d(TAG, "Attempting to stop BTLE scan");
+ try {
+ mBluetoothLeScanner.stopScan(leScanCallback);
+ } catch (IllegalStateException isex) {
+ // when trying to stop a scan while bluetooth is offline
+ // java.lang.IllegalStateException: BT Adapter is not turned ON
+ Log.d(TAG, "Stopping LE scan not possible: " + isex.getMessage());
+ }
+ mLeScanRunning = false;
+ }
+
+ return (mLeScanRunning == isEnabled);
+ }
+
+ private final ScanCallback leScanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ super.onScanResult(callbackType, result);
+ leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
+ }
+
+ @Override
+ public void onBatchScanResults(List<ScanResult> results) {
+ super.onBatchScanResults(results);
+ for (ScanResult result : results)
+ leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
+
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ super.onScanFailed(errorCode);
+ Log.d(TAG, "BTLE device scan failed with " + errorCode);
+ }
+ };
+
+ native void leScanResult(long qtObject, BluetoothDevice device, int rssi, byte[] scanRecord);
+
+ private synchronized void handleOnConnectionStateChange(BluetoothGatt gatt,
+ int status, int newState) {
+
+ Log.d(TAG, "Connection state changes to: " + newState + ", status: " + status
+ + ", qtObject: " + (qtObject != 0));
+ if (qtObject == 0)
+ return;
+
+ int qLowEnergyController_State = 0;
+ //This must be in sync with QLowEnergyController::ControllerState
+ switch (newState) {
+ case BluetoothProfile.STATE_DISCONNECTED:
+ if (bondStateBroadcastReceiver != null) {
+ qtContext.unregisterReceiver(bondStateBroadcastReceiver);
+ bondStateBroadcastReceiver = null;
+ }
+
+ 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.
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.close();
+ if (mHandler != null) {
+ mHandler.getLooper().quitSafely();
+ mHandler = null;
+ }
+ }
+ mBluetoothGatt = null;
+ break;
+ case BluetoothProfile.STATE_CONNECTED:
+ if (bondStateBroadcastReceiver == null) {
+ bondStateBroadcastReceiver = new BondStateBroadcastReceiver();
+ qtContext.registerReceiver(bondStateBroadcastReceiver,
+ new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
+ }
+ qLowEnergyController_State = 2;
+ }
+
+ //This must be in sync with QLowEnergyController::Error
+ int errorCode;
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ errorCode = 0; //QLowEnergyController::NoError
+ break;
+ case BluetoothGatt.GATT_FAILURE: // Android's equivalent of "do not know what error"
+ errorCode = 1; //QLowEnergyController::UnknownError
+ break;
+ case 8: // BLE_HCI_CONNECTION_TIMEOUT
+ Log.w(TAG, "Connection Error: Try to delay connect() call after previous activity");
+ errorCode = 5; //QLowEnergyController::ConnectionError
+ break;
+ case 19: // BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION
+ case 20: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES
+ case 21: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_POWER_OFF
+ Log.w(TAG, "The remote host closed the connection");
+ errorCode = 7; //QLowEnergyController::RemoteHostClosedError
+ break;
+ case 22: // BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION
+ // Internally, Android maps PIN_OR_KEY_MISSING to GATT_CONN_TERMINATE_LOCAL_HOST
+ errorCode = 8; //QLowEnergyController::AuthorizationError
+ break;
+ default:
+ Log.w(TAG, "Unhandled error code on connectionStateChanged: "
+ + status + " " + newState);
+ errorCode = status;
+ break; //TODO deal with all errors
+ }
+ leConnectionStateChange(qtObject, errorCode, qLowEnergyController_State);
+ }
+
+ private synchronized void handleOnServicesDiscovered(BluetoothGatt gatt, int status) {
+ //This must be in sync with QLowEnergyController::Error
+ int errorCode;
+ StringBuilder builder = new StringBuilder();
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ errorCode = 0; //QLowEnergyController::NoError
+ final List<BluetoothGattService> services = mBluetoothGatt.getServices();
+ for (BluetoothGattService service: services) {
+ builder.append(service.getUuid().toString()).append(" "); //space is separator
+ }
+ break;
+ default:
+ Log.w(TAG, "Unhandled error code on onServicesDiscovered: " + status);
+ errorCode = status; break; //TODO deal with all errors
+ }
+ leServicesDiscovered(qtObject, errorCode, builder.toString());
+ if (status == BluetoothGatt.GATT_SUCCESS)
+ scheduleMtuExchange();
+ }
+
+ private synchronized void handleOnCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ byte[] value,
+ int status)
+ {
+ int 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
+ pendingJob = null;
+
+ performNextIO();
+ return;
+ }
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(foundHandle, IoJobType.Read),
+ HANDLE_FOR_RESET);
+ 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 pendingJob -> the timeout has done that already
+ return;
+ }
+
+ GattEntry entry = entries.get(foundHandle);
+ final boolean isServiceDiscoveryRun = !entry.valueKnown;
+ entry.valueKnown = true;
+
+ 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(), value);
+ } 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(), value);
+ } else {
+ // This must be in sync with QLowEnergyService::CharacteristicReadError
+ final int characteristicReadError = 5;
+ leServiceError(qtObject, foundHandle + 1, characteristicReadError);
+ }
+ }
+
+ 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);
+ }
+
+ //unlock the queue for next item
+ pendingJob = null;
+
+ performNextIO();
+ }
+
+ private synchronized void handleOnCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ byte[] value)
+ {
+ int handle = handleForCharacteristic(characteristic);
+ if (handle == -1) {
+ Log.w(TAG,"onCharacteristicChanged: cannot find handle");
+ return;
+ }
+
+ leCharacteristicChanged(qtObject, handle+1, value);
+ }
+
+ private synchronized void handleOnCharacteristicWrite(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;
+ }
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(handle, IoJobType.Write),
+ HANDLE_FOR_RESET);
+ 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 pendingJob -> the timeout has done that already
+ 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
+ }
+
+ byte[] value;
+ value = pendingJob.newValue;
+ pendingJob = null;
+
+ leCharacteristicWritten(qtObject, handle+1, value, errorCode);
+ performNextIO();
+ }
+
+ private synchronized void handleOnDescriptorRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status, byte[] newValue)
+ {
+ int 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
+ pendingJob = null;
+
+ performNextIO();
+ return;
+ }
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(foundHandle, IoJobType.Read),
+ HANDLE_FOR_RESET);
+ 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 pendingJob -> the timeout has done that already
+ return;
+ }
+
+ GattEntry entry = entries.get(foundHandle);
+ final boolean isServiceDiscoveryRun = !entry.valueKnown;
+ entry.valueKnown = true;
+
+ 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(), newValue);
+ } else {
+ if (isServiceDiscoveryRun) {
+ // Cannot read but still advertise the fact that we found a descriptor
+ // The value will be empty.
+ Log.w(TAG, "onDescriptorRead during discovery error: " + status);
+ Log.d(TAG, "Non-readable descriptor " + descriptor.getUuid() +
+ " for characteristic " + descriptor.getCharacteristic().getUuid() +
+ " for service " + descriptor.getCharacteristic().getService().getUuid());
+ leDescriptorRead(qtObject,
+ descriptor.getCharacteristic().getService().getUuid().toString(),
+ descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1,
+ descriptor.getUuid().toString(), newValue);
+ } else {
+ // This must be in sync with QLowEnergyService::DescriptorReadError
+ final int descriptorReadError = 6;
+ leServiceError(qtObject, foundHandle + 1, descriptorReadError);
+ }
+
+ }
+
+ 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);
+ }
+
+ /* 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 = newValue;
+ 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);
+ }
+ }
+ }
+
+ //unlock the queue for next item
+ pendingJob = null;
+
+ performNextIO();
+ }
+
+ private synchronized void handleOnDescriptorWrite(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);
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(handle, IoJobType.Write),
+ HANDLE_FOR_RESET);
+ 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 pendingJob -> the timeout has done that already
+ return;
+ }
+
+ 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
+ }
+
+ byte[] value = pendingJob.newValue;
+ pendingJob = null;
+
+ leDescriptorWritten(qtObject, handle+1, value, errorCode);
+ performNextIO();
+ }
+
+ private synchronized void handleOnMtuChanged(android.bluetooth.BluetoothGatt gatt,
+ int mtu, int status)
+ {
+ int previousMtu = mSupportedMtu;
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ Log.w(TAG, "MTU changed to " + mtu);
+ mSupportedMtu = mtu;
+ } else {
+ Log.w(TAG, "MTU change error " + status + ". New MTU " + mtu);
+ mSupportedMtu = DEFAULT_MTU;
+ }
+ if (previousMtu != mSupportedMtu)
+ leMtuChanged(qtObject, mSupportedMtu);
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(HANDLE_FOR_MTU_EXCHANGE, IoJobType.Mtu), HANDLE_FOR_RESET);
+ if (requestTimedOut) {
+ Log.w(TAG, "Late mtu reply after timeout was hit");
+ // Timeout has hit before this response -> ignore the response
+ // no need to unlock pendingJob -> the timeout has done that already
+ return;
+ }
+
+ pendingJob = null;
+
+ performNextIO();
+ }
+
+ private synchronized void handleOnReadRemoteRssi(android.bluetooth.BluetoothGatt gatt,
+ int rssi, int status)
+ {
+ Log.d(TAG, "RSSI read callback, rssi: " + rssi + ", status: " + status);
+ leRemoteRssiRead(qtObject, rssi, status == BluetoothGatt.GATT_SUCCESS);
+
+ boolean requestTimedOut = !handleForTimeout.compareAndSet(
+ modifiedReadWriteHandle(HANDLE_FOR_RSSI_READ, IoJobType.Rssi), HANDLE_FOR_RESET);
+ if (requestTimedOut) {
+ Log.w(TAG, "Late RSSI read reply after timeout was hit");
+ // Timeout has hit before this response -> ignore the response
+ // no need to unlock pendingJob -> the timeout has done that already
+ return;
+ }
+ pendingJob = null;
+ performNextIO();
+ }
+
+ /*************************************************************/
+ /* Service Discovery */
+ /*************************************************************/
+
+ private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ super.onConnectionStateChange(gatt, status, newState);
+ handleOnConnectionStateChange(gatt, status, newState);
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ super.onServicesDiscovered(gatt, status);
+ handleOnServicesDiscovered(gatt, status);
+
+ }
+
+ @Override
+ // API < 33
+ public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ int status)
+ {
+ super.onCharacteristicRead(gatt, characteristic, status);
+ handleOnCharacteristicRead(gatt, characteristic, characteristic.getValue(), status);
+ }
+
+ @Override
+ // API >= 33
+ public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ byte[] value,
+ int status)
+ {
+ // Note: here we don't call the super implementation as it calls the old "< API 33"
+ // callback, and the callback would be handled twice
+ handleOnCharacteristicRead(gatt, characteristic, value, status);
+ }
+
+ @Override
+ public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ int status)
+ {
+ super.onCharacteristicWrite(gatt, characteristic, status);
+ handleOnCharacteristicWrite(gatt, characteristic, status);
+ }
+
+ // API < 33
+ @Override
+ public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic)
+ {
+ super.onCharacteristicChanged(gatt, characteristic);
+ handleOnCharacteristicChanged(gatt, characteristic, characteristic.getValue());
+ }
+
+ // API >= 33
+ @Override
+ public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattCharacteristic characteristic,
+ byte[] value)
+ {
+ // Note: here we don't call the super implementation as it calls the old "< API 33"
+ // callback, and the callback would be handled twice
+ handleOnCharacteristicChanged(gatt, characteristic, value);
+ }
+
+ // API < 33
+ @Override
+ public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status)
+ {
+ super.onDescriptorRead(gatt, descriptor, status);
+ handleOnDescriptorRead(gatt, descriptor, status, descriptor.getValue());
+ }
+
+ // API >= 33
+ @Override
+ public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status,
+ byte[] value)
+ {
+ // Note: here we don't call the super implementation as it calls the old "< API 33"
+ // callback, and the callback would be handled twice
+ handleOnDescriptorRead(gatt, descriptor, status, value);
+ }
+
+ @Override
+ public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
+ android.bluetooth.BluetoothGattDescriptor descriptor,
+ int status)
+ {
+ super.onDescriptorWrite(gatt, descriptor, status);
+ handleOnDescriptorWrite(gatt, descriptor, status);
+ }
+ //TODO currently not supported
+// @Override
+// void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt,
+// int status) {
+// System.out.println("onReliableWriteCompleted");
+// }
+//
+ @Override
+ public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt, int rssi, int status)
+ {
+ super.onReadRemoteRssi(gatt, rssi, status);
+ handleOnReadRemoteRssi(gatt, rssi, status);
+ }
+
+ @Override
+ public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status)
+ {
+ super.onMtuChanged(gatt, mtu, status);
+ handleOnMtuChanged(gatt, mtu, status);
+ }
+ };
+
+ // This function is called from Qt thread
+ synchronized int mtu() {
+ if (mSupportedMtu == -1) {
+ return DEFAULT_MTU;
+ } else {
+ return mSupportedMtu;
+ }
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean readRemoteRssi() {
+ if (mBluetoothGatt == null)
+ return false;
+
+ // Reading of RSSI can sometimes be 'lost' especially if amidst
+ // characteristic reads/writes ('lost' here meaning that there is no callback).
+ // To avoid this schedule the RSSI read in the job queue.
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.jobType = IoJobType.Rssi;
+ newJob.entry = null;
+
+ if (!readWriteQueue.add(newJob)) {
+ Log.w(TAG, "Cannot add remote RSSI read to queue" );
+ return false;
+ }
+
+ performNextIOThreaded();
+ return true;
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean connect() {
+ BluetoothDevice mRemoteGattDevice;
+
+ if (mBluetoothAdapter == null) {
+ Log.w(TAG, "Cannot connect, no bluetooth adapter");
+ return false;
+ }
+
+ try {
+ mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Remote address is not valid: " + mRemoteGattAddress);
+ return false;
+ }
+
+ /* The required connectGatt function is already available in SDK v26, but Android 8.0
+ * contains a race condition in the Changed callback such that it can return the value that
+ * was written. This is fixed in Android 8.1, which matches SDK v27. */
+ if (Build.VERSION.SDK_INT >= 27) {
+ HandlerThread handlerThread = new HandlerThread("QtBluetoothLEHandlerThread");
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
+
+ Class[] args = new Class[6];
+ args[0] = android.content.Context.class;
+ args[1] = boolean.class;
+ args[2] = android.bluetooth.BluetoothGattCallback.class;
+ args[3] = int.class;
+ args[4] = int.class;
+ args[5] = android.os.Handler.class;
+
+ try {
+ Method connectMethod = mRemoteGattDevice.getClass().getDeclaredMethod("connectGatt", args);
+ if (connectMethod != null) {
+ mBluetoothGatt = (BluetoothGatt) connectMethod.invoke(mRemoteGattDevice, qtContext, false,
+ gattCallback, 2 /* TRANSPORT_LE */, 1 /*BluetoothDevice.PHY_LE_1M*/, mHandler);
+ Log.w(TAG, "Using Android v26 BluetoothDevice.connectGatt()");
+ }
+ } catch (Exception ex) {
+ Log.w(TAG, "connectGatt() v26 not available");
+ ex.printStackTrace();
+ }
+
+ if (mBluetoothGatt == null) {
+ mHandler.getLooper().quitSafely();
+ mHandler = null;
+ }
+ }
+
+ if (mBluetoothGatt == null) {
+ try {
+ //This API element is currently: greylist-max-o (API level 27), reflection, allowed
+ //It may change in the future
+ Class[] constr_args = new Class[5];
+ constr_args[0] = android.bluetooth.BluetoothGattService.class;
+ constr_args[1] = java.util.UUID.class;
+ constr_args[2] = int.class;
+ constr_args[3] = int.class;
+ constr_args[4] = int.class;
+ mCharacteristicConstructor = BluetoothGattCharacteristic.class.getDeclaredConstructor(constr_args);
+ mCharacteristicConstructor.setAccessible(true);
+ } catch (NoSuchMethodException ex) {
+ Log.w(TAG, "Unable get characteristic constructor. Buffer race condition are possible");
+ /* For some reason we don't get the private BluetoothGattCharacteristic ctor.
+ This means that we cannot protect ourselves from issues where concurrent
+ read and write operations on the same char can overwrite each others buffer.
+ Nevertheless we continue with best effort.
+ */
+ }
+ try {
+ mBluetoothGatt =
+ mRemoteGattDevice.connectGatt(qtContext, false,
+ gattCallback, 2 /* TRANSPORT_LE */);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Gatt connection failed");
+ ex.printStackTrace();
+ }
+ }
+ return mBluetoothGatt != null;
+ }
+
+ // This function is called from Qt thread
+ synchronized void disconnect() {
+ if (mBluetoothGatt == null)
+ return;
+
+ mBluetoothGatt.disconnect();
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean discoverServices()
+ {
+ return mBluetoothGatt != null && mBluetoothGatt.discoverServices();
+ }
+
+ private enum GattEntryType
+ {
+ Service, Characteristic, CharacteristicValue, Descriptor
+ }
+ private class GattEntry
+ {
+ GattEntryType type;
+ boolean valueKnown = false;
+ BluetoothGattService service = null;
+ BluetoothGattCharacteristic characteristic = null;
+ BluetoothGattDescriptor descriptor = null;
+ /*
+ * 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.
+ */
+ int endHandle = -1;
+ // pointer back to the handle that describes the service that this GATT entry belongs to
+ int associatedServiceHandle;
+ }
+
+ private enum IoJobType
+ {
+ Read, Write, Mtu,
+ SkippedRead, Rssi
+ // a skipped read is a read which is not executed
+ // introduced in Qt 6.2 to skip reads without changing service discovery logic
+ }
+
+ private class ReadWriteJob
+ {
+ GattEntry entry;
+ byte[] newValue;
+ int requestedWriteType;
+ 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
+ private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>();
+
+
+ private final LinkedList<ReadWriteJob> readWriteQueue = new LinkedList<ReadWriteJob>();
+ private ReadWriteJob pendingJob;
+
+ /*
+ 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;
+ }
+
+ // This function is called from Qt thread (indirectly)
+ 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(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());
+ 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;
+ 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
+ List<BluetoothGattDescriptor> descList = characteristic.getDescriptors();
+ for (BluetoothGattDescriptor desc: descList) {
+ 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.trimToSize();
+ }
+
+ private void resetData()
+ {
+ uuidToEntry.clear();
+ entries.clear();
+ servicesToBeDiscovered.clear();
+
+ // kill all timeout handlers
+ timeoutHandler.removeCallbacksAndMessages(null);
+ handleForTimeout.set(HANDLE_FOR_RESET);
+
+ readWriteQueue.clear();
+ pendingJob = null;
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean discoverServiceDetails(String serviceUuid, boolean fullDiscovery)
+ {
+ Log.d(TAG, "Discover service details for: " + serviceUuid + ", fullDiscovery: "
+ + fullDiscovery + ", BluetoothGatt: " + (mBluetoothGatt != null));
+ 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 discovered or under investigation
+ if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) {
+ Log.w(TAG, "Service already known or to be discovered");
+ return true;
+ }
+
+ servicesToBeDiscovered.add(serviceHandle);
+ scheduleServiceDetailDiscovery(serviceHandle, fullDiscovery);
+ performNextIOThreaded();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ Returns the uuids of the services included by the given service. Otherwise returns null.
+ This function is called from Qt thread
+ */
+ synchronized String includedServices(String serviceUuid)
+ {
+ if (mBluetoothGatt == null)
+ return null;
+
+ 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 synchronized void finishCurrentServiceDiscovery(int handleDiscoveredService)
+ {
+ Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService);
+ GattEntry discoveredService = entries.get(handleDiscoveredService);
+ discoveredService.valueKnown = true;
+ try {
+ 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);
+ }
+
+ // Executes under "this" client mutex. Returns true
+ // if no actual MTU exchange is initiated
+ private boolean executeMtuExchange()
+ {
+ if (mBluetoothGatt.requestMtu(MAX_MTU)) {
+ Log.w(TAG, "MTU change initiated");
+ return false;
+ } else {
+ Log.w(TAG, "MTU change request failed");
+ }
+
+ Log.w(TAG, "Assuming default MTU value of 23 bytes");
+ mSupportedMtu = DEFAULT_MTU;
+ return true;
+ }
+
+ private boolean executeRemoteRssiRead()
+ {
+ if (mBluetoothGatt.readRemoteRssi()) {
+ Log.d(TAG, "RSSI read initiated");
+ return false;
+ }
+ Log.w(TAG, "Initiating remote RSSI read failed");
+ leRemoteRssiRead(qtObject, 0, false);
+ return true;
+ }
+
+ /*
+ * Already executed in GattCallback so executed by the HandlerThread. No need to
+ * post it to the Hander.
+ */
+ private void scheduleMtuExchange() {
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.jobType = IoJobType.Mtu;
+ newJob.entry = null;
+
+ readWriteQueue.add(newJob);
+
+ performNextIO();
+ }
+
+ /*
+ 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.
+
+ */
+ private void scheduleServiceDetailDiscovery(int serviceHandle, boolean fullDiscovery)
+ {
+ GattEntry serviceEntry = entries.get(serviceHandle);
+ final int endHandle = serviceEntry.endHandle;
+
+ if (serviceHandle == endHandle) {
+ Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover");
+ finishCurrentServiceDiscovery(serviceHandle);
+ return;
+ }
+
+ // serviceHandle + 1 -> ignore service handle itself
+ for (int i = serviceHandle + 1; i <= endHandle; i++) {
+ GattEntry entry = entries.get(i);
+
+ if (entry.type == GattEntryType.Service) {
+ // should not really happen unless endHandle is wrong
+ Log.w(TAG, "scheduleServiceDetailDiscovery: wrong endHandle");
+ return;
+ }
+
+ ReadWriteJob newJob = new ReadWriteJob();
+ newJob.entry = entry;
+ if (fullDiscovery) {
+ newJob.jobType = IoJobType.Read;
+ } else {
+ newJob.jobType = IoJobType.SkippedRead;
+ }
+
+ final boolean result = readWriteQueue.add(newJob);
+ if (!result)
+ Log.w(TAG, "Cannot add service discovery job for " + serviceEntry.service.getUuid()
+ + " on item " + entry.type);
+ }
+ }
+
+ /*************************************************************/
+ /* Write Characteristics */
+ /* This function is called from Qt thread */
+ /*************************************************************/
+
+ synchronized 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
+ 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;
+ }
+
+ boolean result;
+ result = readWriteQueue.add(newJob);
+
+ if (!result) {
+ Log.w(TAG, "Cannot add characteristic write request for " + charHandle + " to queue" );
+ return false;
+ }
+
+ performNextIOThreaded();
+ return true;
+ }
+
+ /*************************************************************/
+ /* Write Descriptors */
+ /* This function is called from Qt thread */
+ /*************************************************************/
+
+ synchronized 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;
+ result = readWriteQueue.add(newJob);
+
+ if (!result) {
+ Log.w(TAG, "Cannot add descriptor write request for " + descHandle + " to queue" );
+ return false;
+ }
+
+ performNextIOThreaded();
+ return true;
+ }
+
+ /*************************************************************/
+ /* Read Characteristics */
+ /* This function is called from Qt thread */
+ /*************************************************************/
+
+ synchronized boolean readCharacteristic(int charHandle)
+ {
+ 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.entry = entry;
+ newJob.jobType = IoJobType.Read;
+
+ boolean result;
+ result = readWriteQueue.add(newJob);
+
+ if (!result) {
+ Log.w(TAG, "Cannot add characteristic read request for " + charHandle + " to queue" );
+ return false;
+ }
+
+ performNextIOThreaded();
+ return true;
+ }
+
+ // This function is called from Qt thread
+ synchronized 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;
+ result = readWriteQueue.add(newJob);
+
+ if (!result) {
+ Log.w(TAG, "Cannot add descriptor read request for " + descHandle + " to queue" );
+ return false;
+ }
+
+ performNextIOThreaded();
+ return true;
+ }
+
+ // Called by TimeoutRunnable if the current I/O job timed out.
+ // By the time we reach this point the handleForTimeout counter has already been reset
+ // and the regular responses will be blocked off.
+ private synchronized void interruptCurrentIO(int handle)
+ {
+ //unlock the queue for next item
+ pendingJob = null;
+
+ performNextIOThreaded();
+
+ if (handle == HANDLE_FOR_MTU_EXCHANGE || handle == HANDLE_FOR_RSSI_READ)
+ return;
+
+ try {
+ 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);
+ } catch (IndexOutOfBoundsException outOfBounds) {
+ Log.w(TAG, "interruptCurrentIO(): Unknown gatt entry, index: "
+ + handle + " size: " + entries.size());
+ }
+ }
+
+ /*
+ Wrapper around performNextIO() ensuring that performNextIO() is executed inside
+ the mHandler/mHandlerThread if it exists.
+ */
+ private void performNextIOThreaded()
+ {
+ if (mHandler != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ performNextIO();
+ }
+ });
+ } else {
+ performNextIO();
+ }
+ }
+
+ /*
+ 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 synchronized void performNextIO()
+ {
+ Log.d(TAG, "Perform next BTLE IO, job queue size: " + readWriteQueue.size()
+ + ", a job is pending: " + (pendingJob != null) + ", BluetoothGatt: "
+ + (mBluetoothGatt != null));
+
+ if (mBluetoothGatt == null)
+ return;
+
+ boolean skip = false;
+ final ReadWriteJob nextJob;
+ int handle = HANDLE_FOR_RESET;
+
+ if (readWriteQueue.isEmpty() || pendingJob != null)
+ return;
+
+ nextJob = readWriteQueue.remove();
+ // MTU requests and RSSI reads are special cases
+ if (nextJob.jobType == IoJobType.Mtu) {
+ handle = HANDLE_FOR_MTU_EXCHANGE;
+ } else if (nextJob.jobType == IoJobType.Rssi) {
+ handle = HANDLE_FOR_RSSI_READ;
+ } else {
+ 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
+ timeoutHandler.removeCallbacksAndMessages(null); // remove any timeout handlers
+ handleForTimeout.set(modifiedReadWriteHandle(handle, nextJob.jobType));
+
+ switch (nextJob.jobType) {
+ case Read:
+ skip = executeReadJob(nextJob);
+ break;
+ case SkippedRead:
+ skip = true;
+ break;
+ case Write:
+ skip = executeWriteJob(nextJob);
+ break;
+ case Mtu:
+ skip = executeMtuExchange();
+ case Rssi:
+ skip = executeRemoteRssiRead();
+ break;
+ }
+
+ if (skip) {
+ handleForTimeout.set(HANDLE_FOR_RESET); // not a pending call -> release atomic
+ } else {
+ pendingJob = nextJob;
+ timeoutHandler.postDelayed(new TimeoutRunnable(
+ modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT);
+ }
+
+ if (nextJob.jobType != IoJobType.Mtu && nextJob.jobType != IoJobType.Rssi) {
+ Log.d(TAG, "Performing queued job, handle: " + handle + " " + nextJob.jobType + " (" +
+ (nextJob.requestedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) +
+ ") ValueKnown: " + nextJob.entry.valueKnown + " Skipping: " + skip +
+ " " + 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. The error report is not required during
+ the initial service discovery though.
+ */
+ if (handle > HANDLE_FOR_RESET) {
+ // 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 (isServiceDiscovery) {
+ entry.valueKnown = true;
+ switch (entry.type) {
+ case Characteristic:
+ Log.d(TAG,
+ nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
+ + " 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(), null);
+ break;
+ case Descriptor:
+ Log.d(TAG,
+ nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
+ + " 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(),
+ null);
+ break;
+ case CharacteristicValue:
+ // for more details see scheduleServiceDetailDiscovery(int, boolean)
+ break;
+ case Service:
+ Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen.");
+ break;
+ }
+
+ // last entry of current discovery run?
+ try {
+ GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
+ if (serviceEntry.endHandle == handle)
+ finishCurrentServiceDiscovery(entry.associatedServiceHandle);
+ } catch (IndexOutOfBoundsException outOfBounds) {
+ Log.w(TAG, "performNextIO(): Unknown service for entry, index: "
+ + entry.associatedServiceHandle + " size: " + entries.size());
+ }
+ } else {
+ int errorCode = 0;
+
+ // The error codes below must be in sync with QLowEnergyService::ServiceError
+ if (nextJob.jobType == IoJobType.Read) {
+ errorCode = (entry.type == GattEntryType.Characteristic) ?
+ 5 : 6; // CharacteristicReadError : DescriptorReadError
+ } else {
+ errorCode = (entry.type == GattEntryType.Characteristic) ?
+ 2 : 3; // CharacteristicWriteError : DescriptorWriteError
+ }
+
+ leServiceError(qtObject, handle + 1, errorCode);
+ }
+ }
+
+ performNextIO();
+ }
+ }
+
+ private BluetoothGattCharacteristic cloneChararacteristic(BluetoothGattCharacteristic other) {
+ try {
+ return (BluetoothGattCharacteristic) mCharacteristicConstructor.newInstance(other.getService(),
+ other.getUuid(), other.getInstanceId(), other.getProperties(), other.getPermissions());
+ } catch (Exception ex) {
+ Log.w(TAG, "Cloning characteristic failed!" + ex);
+ return null;
+ }
+ }
+
+ // Returns true if nextJob should be skipped.
+ private boolean executeWriteJob(ReadWriteJob nextJob)
+ {
+ boolean result;
+ switch (nextJob.entry.type) {
+ case Characteristic:
+ if (Build.VERSION.SDK_INT >= 33) {
+ int writeResult = mBluetoothGatt.writeCharacteristic(
+ nextJob.entry.characteristic, nextJob.newValue, nextJob.requestedWriteType);
+ return (writeResult != BluetoothStatusCodes.SUCCESS);
+ }
+ if (mHandler != null || mCharacteristicConstructor == null) {
+ if (nextJob.entry.characteristic.getWriteType() != nextJob.requestedWriteType) {
+ nextJob.entry.characteristic.setWriteType(nextJob.requestedWriteType);
+ }
+ result = nextJob.entry.characteristic.setValue(nextJob.newValue);
+ return !result || !mBluetoothGatt.writeCharacteristic(nextJob.entry.characteristic);
+ } else {
+ BluetoothGattCharacteristic orig = nextJob.entry.characteristic;
+ BluetoothGattCharacteristic tmp = cloneChararacteristic(orig);
+ if (tmp == null)
+ return true;
+ tmp.setWriteType(nextJob.requestedWriteType);
+ return !tmp.setValue(nextJob.newValue) || !mBluetoothGatt.writeCharacteristic(tmp);
+ }
+ 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);
+ }
+
+ if (Build.VERSION.SDK_INT >= 33) {
+ int writeResult = mBluetoothGatt.writeDescriptor(
+ nextJob.entry.descriptor, nextJob.newValue);
+ return (writeResult != BluetoothStatusCodes.SUCCESS);
+ }
+ 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;
+ }
+
+ // Returns true if nextJob should be skipped.
+ private boolean executeReadJob(ReadWriteJob nextJob)
+ {
+ boolean result;
+ switch (nextJob.entry.type) {
+ case 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:
+ 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:
+ 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;
+ case Mtu:
+ modifiedHandle = HANDLE_FOR_MTU_EXCHANGE;
+ break;
+ case Rssi:
+ modifiedHandle = HANDLE_FOR_RSSI_READ;
+ break;
+ }
+
+ return modifiedHandle;
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean requestConnectionUpdatePriority(double minimalInterval)
+ {
+ if (mBluetoothGatt == 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
+
+ try {
+ return mBluetoothGatt.requestConnectionPriority(requestPriority);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Connection update priority out of range: " + requestPriority);
+ return false;
+ }
+ }
+
+ native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState);
+ native void leMtuChanged(long qtObject, int mtu);
+ native void leRemoteRssiRead(long qtObject, int rssi, boolean success);
+ native void leServicesDiscovered(long qtObject, int errorCode, String uuidList);
+ native void leServiceDetailDiscoveryFinished(long qtObject, final String serviceUuid,
+ int startHandle, int endHandle);
+ native void leCharacteristicRead(long qtObject, String serviceUuid,
+ int charHandle, String charUuid,
+ int properties, byte[] data);
+ native void leDescriptorRead(long qtObject, String serviceUuid, String charUuid,
+ int descHandle, String descUuid, byte[] data);
+ native void leCharacteristicWritten(long qtObject, int charHandle, byte[] newData,
+ int errorCode);
+ native void leDescriptorWritten(long qtObject, int charHandle, byte[] newData,
+ int errorCode);
+ native void leCharacteristicChanged(long qtObject, int charHandle, byte[] newData);
+ native void leServiceError(long qtObject, int attributeHandle, int errorCode);
+}
+
diff --git a/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java
new file mode 100644
index 00000000..ae557de6
--- /dev/null
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothLEServer.java
@@ -0,0 +1,989 @@
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.content.Context;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseData.Builder;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.os.ParcelUuid;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.HashMap;
+import java.util.UUID;
+
+class QtBluetoothLEServer {
+ private static final String TAG = "QtBluetoothGattServer";
+
+ /* Pointer to the Qt object that "owns" the Java object */
+ @SuppressWarnings({"CanBeFinal", "WeakerAccess"})
+ long qtObject = 0;
+ @SuppressWarnings("WeakerAccess")
+
+ private Context qtContext = null;
+
+ // Bluetooth members
+ private BluetoothAdapter mBluetoothAdapter = null;
+ private BluetoothManager mBluetoothManager = null;
+ private BluetoothGattServer mGattServer = null;
+ private BluetoothLeAdvertiser mLeAdvertiser = null;
+
+ private ArrayList<BluetoothGattService> mPendingServiceAdditions =
+ new ArrayList<BluetoothGattService>();
+
+ private String mRemoteName = "";
+ // This function is called from Qt thread
+ synchronized String remoteName() {
+ return mRemoteName;
+ }
+
+ private String mRemoteAddress = "";
+ // This function is called from Qt thread
+ synchronized String remoteAddress() {
+ return mRemoteAddress;
+ }
+
+ // BT Core v5.3, 5.2.1, Vol 3, Part G
+ private static final int DEFAULT_LE_ATT_MTU = 23;
+ // Holds the currently supported/used MTU
+ private int mSupportedMtu = DEFAULT_LE_ATT_MTU;
+ // Implementation defined limit
+ private static final int MAX_PENDING_WRITE_COUNT = 1024;
+ // BT Core v5.3, 3.4.6.1, Vol 3, Part F
+ private static final int GATT_ERROR_PREPARE_QUEUE_FULL = 0x9;
+ // BT Core v5.3, 3.2.9, Vol 3, Part F
+ private static final int BTLE_MAX_ATTRIBUTE_VALUE_SIZE = 512;
+
+ // The class stores queued writes from the remote device. The writes are
+ // executed later when instructed to do so by onExecuteWrite() callback.
+ private class WriteEntry {
+ WriteEntry(BluetoothDevice remoteDevice, Object target) {
+ this.remoteDevice = remoteDevice;
+ this.target = target;
+ this.writes = new ArrayList<Pair<byte[], Integer>>();
+ }
+ // Returns true if this is a proper entry for given device + target
+ boolean match(BluetoothDevice device, Object target) {
+ return remoteDevice.equals(device) && target.equals(target);
+ }
+ final BluetoothDevice remoteDevice; // Device that issued the writes
+ final Object target; // Characteristic or Descriptor
+ final List<Pair<byte[], Integer>> writes; // Value, offset
+ }
+ private final List<WriteEntry> mPendingPreparedWrites = new ArrayList<>();
+
+ // Helper function to clear the pending writes of a remote device. If the provided device
+ // is null, all writes are cleared
+ private void clearPendingPreparedWrites(Object device) {
+ if (device == null)
+ mPendingPreparedWrites.clear();
+ ListIterator<WriteEntry> iterator = mPendingPreparedWrites.listIterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().remoteDevice.equals(device))
+ iterator.remove();
+ }
+ }
+
+ // The function adds a 'prepared write' entry to target's queue. If the "target + device"
+ // didn't have a queue before (this being the first write), the queue is created.
+ // Targets must be either descriptors or characteristics.
+ private int addPendingPreparedWrite(BluetoothDevice device, Object target,
+ int offset, byte[] value) {
+ WriteEntry entry = null;
+ int currentWriteCount = 0;
+
+ // Try to find an existing matching entry. Also while looping, count
+ // the total number of writes so far in order to know if we exceed the
+ // write queue size we have set for ourselves
+ for (WriteEntry e : mPendingPreparedWrites) {
+ if (e.match(device, target))
+ entry = e;
+ currentWriteCount += e.writes.size();
+ }
+
+ // BT Core v5.3, 3.4.6.1, Vol 3, Part F
+ if (currentWriteCount > MAX_PENDING_WRITE_COUNT) {
+ Log.w(TAG, "Prepared write queue is full, returning an error.");
+ return GATT_ERROR_PREPARE_QUEUE_FULL;
+ }
+
+ // If no matching entry, create a new one. This means this is the first prepared
+ // write request to this "device + target" combination
+ if (entry == null)
+ mPendingPreparedWrites.add(entry = new WriteEntry(device, target));
+
+ // Append the newly received chunk of data along with its offset
+ entry.writes.add(new Pair<byte[], Integer>(value, offset));
+ return BluetoothGatt.GATT_SUCCESS;
+ }
+
+ /*
+ As per Bluetooth specification each connected device can have individual and persistent
+ Client characteristic configurations (see Bluetooth Spec 5.0 Vol 3 Part G 3.3.3.3)
+ This class manages the existing configurrations.
+ */
+ private class ClientCharacteristicManager {
+ private final HashMap<BluetoothGattCharacteristic, List<Entry>> notificationStore = new HashMap<BluetoothGattCharacteristic, List<Entry>>();
+
+ private class Entry {
+ BluetoothDevice device = null;
+ byte[] value = null;
+ boolean isConnected = false;
+ }
+
+ void insertOrUpdate(BluetoothGattCharacteristic characteristic,
+ BluetoothDevice device, byte[] newValue)
+ {
+ if (notificationStore.containsKey(characteristic)) {
+
+ List<Entry> entries = notificationStore.get(characteristic);
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).device.equals(device)) {
+ Entry e = entries.get(i);
+ e.value = newValue;
+ entries.set(i, e);
+ return;
+ }
+ }
+
+ // not match so far -> add device to list
+ Entry e = new Entry();
+ e.device = device;
+ e.value = newValue;
+ e.isConnected = true;
+ entries.add(e);
+ return;
+ }
+
+ // new characteristic
+ Entry e = new Entry();
+ e.device = device;
+ e.value = newValue;
+ e.isConnected = true;
+ List<Entry> list = new LinkedList<Entry>();
+ list.add(e);
+ notificationStore.put(characteristic, list);
+ }
+
+ /*
+ Marks client characteristic configuration entries as (in)active based the associated
+ devices general connectivity state.
+ This function avoids that existing configurations are not acted
+ upon when the associated device is not connected.
+ */
+ void markDeviceConnectivity(BluetoothDevice device, boolean isConnected)
+ {
+ final Iterator<BluetoothGattCharacteristic> keys = notificationStore.keySet().iterator();
+ while (keys.hasNext()) {
+ final BluetoothGattCharacteristic characteristic = keys.next();
+ final List<Entry> entries = notificationStore.get(characteristic);
+ if (entries == null)
+ continue;
+
+ ListIterator<Entry> charConfig = entries.listIterator();
+ while (charConfig.hasNext()) {
+ Entry e = charConfig.next();
+ if (e.device.equals(device))
+ e.isConnected = isConnected;
+ }
+ }
+ }
+
+ // Returns list of all BluetoothDevices which require notification or indication.
+ // No match returns an empty list.
+ List<BluetoothDevice> getToBeUpdatedDevices(BluetoothGattCharacteristic characteristic)
+ {
+ ArrayList<BluetoothDevice> result = new ArrayList<BluetoothDevice>();
+ if (!notificationStore.containsKey(characteristic))
+ return result;
+
+ final ListIterator<Entry> iter = notificationStore.get(characteristic).listIterator();
+ while (iter.hasNext())
+ result.add(iter.next().device);
+
+ return result;
+ }
+
+ // Returns null if no match; otherwise the configured actual client characteristic
+ // configuration value
+ byte[] valueFor(BluetoothGattCharacteristic characteristic, BluetoothDevice device)
+ {
+ if (!notificationStore.containsKey(characteristic))
+ return null;
+
+ List<Entry> entries = notificationStore.get(characteristic);
+ for (int i = 0; i < entries.size(); i++) {
+ final Entry entry = entries.get(i);
+ if (entry.device.equals(device) && entry.isConnected == true)
+ return entries.get(i).value;
+ }
+
+ return null;
+ }
+ }
+
+ private static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID
+ .fromString("00002902-0000-1000-8000-00805f9b34fb");
+ ClientCharacteristicManager clientCharacteristicManager = new ClientCharacteristicManager();
+
+ QtBluetoothLEServer(Context context)
+ {
+ qtContext = context;
+ if (qtContext == null) {
+ Log.w(TAG, "Missing context object. Peripheral role disabled.");
+ return;
+ }
+
+ mBluetoothManager =
+ (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+ if (mBluetoothManager == null) {
+ Log.w(TAG, "Bluetooth service not available. Peripheral role disabled.");
+ return;
+ }
+
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ if (mBluetoothAdapter == null) {
+ Log.w(TAG, "Missing Bluetooth adapter. Peripheral role disabled.");
+ return;
+ }
+
+ Log.w(TAG, "Let's do BTLE Peripheral.");
+ }
+
+ // The following functions are synchronized callback handlers. The callbacks
+ // from Android are forwarded to these methods to synchronize member variable
+ // access with other threads (the Qt thread's JNI calls in particular).
+ //
+ // We use a single lock object (this server) for simplicity because:
+ // - Some variables may change and would thus not be suitable as locking objects but
+ // would require their own additional objects => overhead
+ // - Many accesses to shared variables are infrequent and the code paths are fast and
+ // deterministic meaning that long "wait times" on a lock should not happen
+ // - Typically several shared variables are accessed in a single code block.
+ // If each variable would be protected individually, the amount of (nested) locking
+ // would become quite unreasonable
+
+ synchronized void handleOnConnectionStateChange(BluetoothDevice device,
+ int status, int newState)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring connection state event, server is disconnected");
+ return;
+ }
+ // Multiple GATT devices may be connected. Check if we still have connected
+ // devices or not, and set the server state accordingly. Note: it seems we get
+ // notifications from all GATT clients, not just from the ones interested in
+ // the services provided by this BT LE Server. Furthermore the list of
+ // currently connected devices does not appear to be in any particular order.
+ List<BluetoothDevice> connectedDevices =
+ mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
+ Log.w(TAG, "Device " + device + " connection state: " + newState + ", status: "
+ + status + ", connected devices: " + connectedDevices);
+ // 0 == QLowEnergyController::UnconnectedState
+ // 2 == QLowEnergyController::ConnectedState
+ int qtControllerState = connectedDevices.size() > 0 ? 2 : 0;
+
+ switch (newState) {
+ case BluetoothProfile.STATE_CONNECTED:
+ clientCharacteristicManager.markDeviceConnectivity(device, true);
+ mRemoteName = device.getName();
+ mRemoteAddress = device.getAddress();
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ clientCharacteristicManager.markDeviceConnectivity(device, false);
+ clearPendingPreparedWrites(device);
+ // Update the remoteAddress and remoteName if needed
+ if (device.getAddress().equals(mRemoteAddress)
+ && !connectedDevices.isEmpty()) {
+ mRemoteName = connectedDevices.get(0).getName();
+ mRemoteAddress = connectedDevices.get(0).getAddress();
+ }
+ break;
+ default:
+ // According to the API doc of this callback this should not happen
+ Log.w(TAG, "Unhandled connection state change: " + newState);
+ return;
+ }
+
+ // If last client disconnected, close down the server
+ if (qtControllerState == 0) { // QLowEnergyController::UnconnectedState
+ mPendingServiceAdditions.clear();
+ mGattServer.close();
+ mGattServer = null;
+ mRemoteName = "";
+ mRemoteAddress = "";
+ mSupportedMtu = DEFAULT_LE_ATT_MTU;
+ }
+
+ int qtErrorCode;
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS:
+ qtErrorCode = 0;
+ break;
+ default:
+ Log.w(TAG, "Unhandled error code on peripheral connectionStateChanged: "
+ + status + " " + newState);
+ qtErrorCode = status;
+ break;
+ }
+
+ leConnectionStateChange(qtObject, qtErrorCode, qtControllerState);
+ }
+
+ synchronized void handleOnServiceAdded(int status, BluetoothGattService service)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring service addition event, server is disconnected");
+ return;
+ }
+
+ Log.d(TAG, "Service " + service.getUuid().toString() + " addition result: " + status);
+
+ // Remove the indicated service from the pending queue
+ ListIterator<BluetoothGattService> iterator = mPendingServiceAdditions.listIterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().getUuid().equals(service.getUuid())) {
+ iterator.remove();
+ break;
+ }
+ }
+
+ // If there are more services in the queue, add the next whose add initiation succeeds
+ iterator = mPendingServiceAdditions.listIterator();
+ while (iterator.hasNext()) {
+ BluetoothGattService nextService = iterator.next();
+ if (mGattServer.addService(nextService)) {
+ break;
+ } else {
+ Log.w(TAG, "Adding service " + nextService.getUuid().toString() + " failed");
+ iterator.remove();
+ }
+ }
+ }
+
+ synchronized void handleOnCharacteristicReadRequest(BluetoothDevice device,
+ int requestId, int offset,
+ BluetoothGattCharacteristic characteristic)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring characteristic read, server is disconnected");
+ return;
+ }
+
+ byte[] characteristicData =
+ ((QtBluetoothGattCharacteristic)characteristic).getLocalValue();
+
+ try {
+ byte[] dataArray = Arrays.copyOfRange(characteristicData,
+ offset, characteristicData.length);
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+ offset, dataArray);
+ } catch (Exception ex) {
+ Log.w(TAG, "onCharacteristicReadRequest: " + requestId + " "
+ + offset + " " + characteristicData.length);
+ ex.printStackTrace();
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE,
+ offset, null);
+ }
+ }
+
+ synchronized void handleOnCharacteristicWriteRequest(BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring characteristic write, server is disconnected");
+ return;
+ }
+ Log.w(TAG, "onCharacteristicWriteRequest " + preparedWrite + " " + offset + " "
+ + value.length);
+ final int minValueLen = ((QtBluetoothGattCharacteristic)characteristic).minValueLength;
+ final int maxValueLen = ((QtBluetoothGattCharacteristic)characteristic).maxValueLength;
+
+ int resultStatus = BluetoothGatt.GATT_SUCCESS;
+ boolean sendNotificationOrIndication = false;
+
+ if (!preparedWrite) { // regular write
+ // User may have defined minimum and maximum size for the value, which
+ // we enforce here. If the user has not defined these limits, the default
+ // values 0..INT_MAX do not limit anything.
+ if (value.length < minValueLen || value.length > maxValueLen) {
+ // BT Core v 5.3, 4.9.3, Vol 3, Part G
+ Log.w(TAG, "onCharacteristicWriteRequest invalid char value length: "
+ + value.length + ", min: " + minValueLen + ", max: " + maxValueLen);
+ resultStatus = BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
+ } else if (offset == 0) {
+ ((QtBluetoothGattCharacteristic)characteristic).setLocalValue(value);
+ leServerCharacteristicChanged(qtObject, characteristic, value);
+ sendNotificationOrIndication = true;
+ } else {
+ // This should not really happen as per Bluetooth spec
+ Log.w(TAG, "onCharacteristicWriteRequest: !preparedWrite, offset "
+ + offset + ", Not supported");
+ resultStatus = BluetoothGatt.GATT_INVALID_OFFSET;
+ }
+ } else {
+ // BT Core v5.3, 3.4.6, Vol 3, Part F
+ // This is a prepared write which is used to write characteristics larger than
+ // MTU. We need to record all requests and execute them in one go once
+ // onExecuteWrite() is received. We use a queue to remember the pending
+ // requests.
+ resultStatus = addPendingPreparedWrite(device, characteristic, offset, value);
+ }
+
+ if (responseNeeded)
+ mGattServer.sendResponse(device, requestId, resultStatus, offset, value);
+ if (sendNotificationOrIndication)
+ sendNotificationsOrIndications(characteristic);
+ }
+
+ synchronized void handleOnDescriptorReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattDescriptor descriptor)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring descriptor read, server is disconnected");
+ return;
+ }
+
+ byte[] dataArray = ((QtBluetoothGattDescriptor)descriptor).getLocalValue();
+
+ try {
+ if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
+ dataArray = clientCharacteristicManager.valueFor(
+ descriptor.getCharacteristic(), device);
+ if (dataArray == null)
+ dataArray = ((QtBluetoothGattDescriptor)descriptor).getLocalValue();
+ }
+
+ dataArray = Arrays.copyOfRange(dataArray, offset, dataArray.length);
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+ offset, dataArray);
+ } catch (Exception ex) {
+ Log.w(TAG, "onDescriptorReadRequest: " + requestId + " "
+ + offset + " " + dataArray.length);
+ ex.printStackTrace();
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE,
+ offset, null);
+ }
+ }
+
+ synchronized void handleOnDescriptorWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattDescriptor descriptor, boolean preparedWrite,
+ boolean responseNeeded, int offset, byte[] value)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring descriptor write, server is disconnected");
+ return;
+ }
+
+ Log.w(TAG, "onDescriptorWriteRequest " + preparedWrite + " " + offset + " " + value.length);
+ int resultStatus = BluetoothGatt.GATT_SUCCESS;
+
+ if (!preparedWrite) { // regular write
+ if (offset == 0) {
+ if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
+ // If both IND and NTF are requested, resort to NTF only. BT
+ // specification does not prohibit nor mention using both, but it is
+ // unlikely what the client intended. Stack behaviours vary;
+ // Apple client-side stack does not allow this, while Bluez client-side
+ // stack erroneously sends this even if the developer only asked for
+ // the other. The 0x03 value is a bitwise combination of 0x01 and 0x02
+ // as per specification: BT Core v5.3, 3.3.3.3, Vol 3, Part G
+ if (value[0] == 0x03) {
+ Log.w(TAG, "Warning: In CCC of characteristic: "
+ + descriptor.getCharacteristic().getUuid()
+ + " enabling both NTF & IND requested, enabling NTF only.");
+ value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
+ }
+ clientCharacteristicManager.insertOrUpdate(
+ descriptor.getCharacteristic(),
+ device, value);
+ }
+ ((QtBluetoothGattDescriptor)descriptor).setLocalValue(value);
+ leServerDescriptorWritten(qtObject, descriptor, value);
+ } else {
+ // This should not really happen as per Bluetooth spec
+ Log.w(TAG, "onDescriptorWriteRequest: !preparedWrite, offset "
+ + offset + ", Not supported");
+ resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
+ }
+ } else {
+ // BT Core v5.3, 3.4.6, Vol 3, Part F
+ // This is a prepared write which is used to write descriptors larger than MTU.
+ // We need to record all requests and execute them in one go once
+ // onExecuteWrite() is received. We use a queue to remember the pending
+ // requests.
+ resultStatus = addPendingPreparedWrite(device, descriptor, offset, value);
+ }
+
+ if (responseNeeded)
+ mGattServer.sendResponse(device, requestId, resultStatus, offset, value);
+ }
+
+ synchronized void handleOnExecuteWrite(BluetoothDevice device,
+ int requestId, boolean execute)
+ {
+ if (mGattServer == null) {
+ Log.w(TAG, "Ignoring execute write, server is disconnected");
+ return;
+ }
+
+ Log.w(TAG, "onExecuteWrite " + device + " " + requestId + " " + execute);
+
+ if (execute) {
+ // BT Core v5.3, 3.4.6.3, Vol 3, Part F
+ // Execute all pending prepared writes for the provided 'device'
+ for (WriteEntry entry : mPendingPreparedWrites) {
+ if (!entry.remoteDevice.equals(device))
+ continue;
+
+ byte[] newValue = null;
+ // The target can be a descriptor or a characteristic
+ byte[] currentValue = (entry.target instanceof BluetoothGattCharacteristic)
+ ? ((QtBluetoothGattCharacteristic)entry.target).getLocalValue()
+ : ((QtBluetoothGattDescriptor)entry.target).getLocalValue();
+
+ // Iterate writes and apply them to the currentValue in received order
+ for (Pair<byte[], Integer> write : entry.writes) {
+ // write.first is data, write.second.intValue() is offset. Check
+ // that the offset is not beyond the length of the current value
+ if (write.second.intValue() > currentValue.length) {
+ clearPendingPreparedWrites(device);
+ // BT Core v5.3, 3.4.6.3, Vol 3, Part F
+ mGattServer.sendResponse(device, requestId,
+ BluetoothGatt.GATT_INVALID_OFFSET,
+ 0, null);
+ return;
+ }
+
+ // User may have defined value minimum and maximum sizes for
+ // characteristics, which we enforce here. If the user has not defined
+ // these limits, the default values 0..INT_MAX do not limit anything.
+ // The value size cannot decrease in prepared write (small write is a
+ // partial update) => no check for the minimum size limit here.
+ if (entry.target instanceof QtBluetoothGattCharacteristic &&
+ (write.second.intValue() + write.first.length >
+ ((QtBluetoothGattCharacteristic)entry.target).maxValueLength)) {
+ clearPendingPreparedWrites(device);
+ // BT Core v5.3, 3.4.6.3, Vol 3, Part F
+ mGattServer.sendResponse(device, requestId,
+ BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH,
+ 0, null);
+ return;
+ }
+
+ // Determine the size of the new value as we may be extending the
+ // current value size
+ newValue = new byte[Math.max(write.second.intValue() +
+ write.first.length, currentValue.length)];
+ // Copy the current value to the newValue. We can't use the currentValue
+ // directly because the length of value might increase by this write
+ System.arraycopy(currentValue, 0, newValue, 0, currentValue.length);
+ // Apply this iteration's write to the newValue
+ System.arraycopy(write.first, 0, newValue, write.second.intValue(),
+ write.first.length);
+ // Update the currentValue as there may be more writes to apply
+ currentValue = newValue;
+ }
+
+ // Update value and inform the Qt/C++ side on the update
+ if (entry.target instanceof BluetoothGattCharacteristic) {
+ ((QtBluetoothGattCharacteristic)entry.target).setLocalValue(newValue);
+ leServerCharacteristicChanged(
+ qtObject, (BluetoothGattCharacteristic)entry.target, newValue);
+ } else {
+ ((QtBluetoothGattDescriptor)entry.target).setLocalValue(newValue);
+ leServerDescriptorWritten(
+ qtObject, (BluetoothGattDescriptor)entry.target, newValue);
+ }
+ }
+ }
+ // Either we executed all writes or were asked to cancel.
+ // In any case clear writes and respond.
+ clearPendingPreparedWrites(device);
+ mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
+ }
+
+ synchronized void handleOnMtuChanged(BluetoothDevice device, int mtu)
+ {
+ if (mSupportedMtu == mtu)
+ return;
+ mSupportedMtu = mtu;
+ leMtuChanged(qtObject, mSupportedMtu);
+ }
+
+ /*
+ * Call back handler for the Gatt Server.
+ */
+ private BluetoothGattServerCallback mGattServerListener = new BluetoothGattServerCallback()
+ {
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+ super.onConnectionStateChange(device, status, newState);
+ handleOnConnectionStateChange(device, status, newState);
+ }
+
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ super.onServiceAdded(status, service);
+ handleOnServiceAdded(status, service);
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic)
+ {
+ super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
+ handleOnCharacteristicReadRequest(device, requestId, offset, characteristic);
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)
+ {
+ super.onCharacteristicWriteRequest(device, requestId, characteristic,
+ preparedWrite, responseNeeded, offset, value);
+ handleOnCharacteristicWriteRequest(device, requestId, characteristic,
+ preparedWrite, responseNeeded, offset, value);
+ }
+
+ @Override
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor)
+ {
+ super.onDescriptorReadRequest(device, requestId, offset, descriptor);
+ handleOnDescriptorReadRequest(device, requestId, offset, descriptor);
+ }
+
+ @Override
+ public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor,
+ boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)
+ {
+ super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite,
+ responseNeeded, offset, value);
+ handleOnDescriptorWriteRequest(device, requestId, descriptor, preparedWrite,
+ responseNeeded, offset, value);
+ }
+
+ @Override
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute)
+ {
+ super.onExecuteWrite(device, requestId, execute);
+ handleOnExecuteWrite(device, requestId, execute);
+ }
+
+ @Override
+ public void onNotificationSent(BluetoothDevice device, int status) {
+ super.onNotificationSent(device, status);
+ Log.w(TAG, "onNotificationSent" + device + " " + status);
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothDevice device, int mtu) {
+ handleOnMtuChanged(device, mtu);
+ }
+ };
+
+ // This function is called from Qt thread
+ synchronized int mtu() {
+ return mSupportedMtu;
+ }
+
+ // This function is called from Qt thread
+ synchronized boolean connectServer()
+ {
+ if (mGattServer != null)
+ return true;
+
+ BluetoothManager manager = (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+ if (manager == null) {
+ Log.w(TAG, "Bluetooth service not available.");
+ return false;
+ }
+
+ mGattServer = manager.openGattServer(qtContext, mGattServerListener);
+
+ return (mGattServer != null);
+ }
+
+ // This function is called from Qt thread
+ synchronized void disconnectServer()
+ {
+ if (mGattServer == null)
+ return;
+
+ clearPendingPreparedWrites(null);
+ mPendingServiceAdditions.clear();
+ mGattServer.close();
+ mGattServer = null;
+
+ mRemoteName = mRemoteAddress = "";
+ leConnectionStateChange(qtObject, 0 /*NoError*/,
+ 0 /*QLowEnergyController::UnconnectedState*/);
+ }
+
+ // This function is called from Qt thread
+ boolean startAdvertising(AdvertiseData advertiseData,
+ AdvertiseData scanResponse,
+ AdvertiseSettings settings)
+ {
+ // Check that the bluetooth is on
+ if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
+ Log.w(TAG, "StartAdvertising: Bluetooth not available or offline");
+ return false;
+ }
+
+ // According to Android doc this check should always precede the advertiser creation
+ if (mLeAdvertiser == null && mBluetoothAdapter.isMultipleAdvertisementSupported())
+ mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
+
+ if (mLeAdvertiser == null) {
+ Log.w(TAG, "StartAdvertising: LE advertisement not supported");
+ return false;
+ }
+
+ if (!connectServer()) {
+ Log.w(TAG, "Server::startAdvertising: Cannot open GATT server");
+ return false;
+ }
+
+ Log.w(TAG, "Starting to advertise.");
+ mLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseListener);
+
+ return true;
+ }
+
+ // This function is called from Qt thread
+ void stopAdvertising()
+ {
+ if (mLeAdvertiser == null)
+ return;
+
+ mLeAdvertiser.stopAdvertising(mAdvertiseListener);
+ Log.w(TAG, "Advertisement stopped.");
+ }
+
+ // This function is called from Qt thread
+ synchronized void addService(BluetoothGattService service)
+ {
+ if (!connectServer()) {
+ Log.w(TAG, "Server::addService: Cannot open GATT server");
+ return;
+ }
+
+ // When we add a service, we must wait for onServiceAdded callback before adding the
+ // next one. If the pending service queue is empty it means that there are no ongoing
+ // service additions => add the service to the server. If there are services in the
+ // queue it means there is an initiated addition ongoing, and we only add to the queue.
+ if (mPendingServiceAdditions.isEmpty()) {
+ if (mGattServer.addService(service))
+ mPendingServiceAdditions.add(service);
+ else
+ Log.w(TAG, "Adding service " + service.getUuid().toString() + " failed.");
+ } else {
+ mPendingServiceAdditions.add(service);
+ }
+ }
+
+ /*
+ Check the client characteristics configuration for the given characteristic
+ and sends notifications or indications as per required.
+
+ This function is called from Qt and Java threads and calls must be protected
+ */
+ private void sendNotificationsOrIndications(BluetoothGattCharacteristic characteristic)
+ {
+ final ListIterator<BluetoothDevice> iter =
+ clientCharacteristicManager.getToBeUpdatedDevices(characteristic).listIterator();
+
+ // TODO This quick loop over multiple devices should be synced with onNotificationSent().
+ // The next notifyCharacteristicChanged() call must wait until onNotificationSent()
+ // was received. At this becomes an issue when the server accepts multiple remote
+ // devices at the same time.
+ while (iter.hasNext()) {
+ final BluetoothDevice device = iter.next();
+ final byte[] clientCharacteristicConfig =
+ clientCharacteristicManager.valueFor(characteristic, device);
+ if (clientCharacteristicConfig != null) {
+ if (Arrays.equals(clientCharacteristicConfig,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, false,
+ ((QtBluetoothGattCharacteristic)characteristic).getLocalValue());
+ } else {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, false);
+ }
+ } else if (Arrays.equals(clientCharacteristicConfig,
+ BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, true,
+ ((QtBluetoothGattCharacteristic)characteristic).getLocalValue());
+ } else {
+ mGattServer.notifyCharacteristicChanged(device, characteristic, true);
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ Updates the local database value for the given characteristic with \a charUuid and
+ \a newValue. If notifications for this task are enabled an appropriate notification will
+ be send to the remote client.
+
+ This function is called from the Qt thread.
+ */
+ boolean writeCharacteristic(BluetoothGattService service, UUID charUuid, byte[] newValue)
+ {
+ BluetoothGattCharacteristic foundChar = null;
+ List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
+ for (BluetoothGattCharacteristic iter: charList) {
+ if (iter.getUuid().equals(charUuid) && foundChar == null) {
+ foundChar = iter;
+ // don't break here since we want to check next condition below on next iteration
+ } else if (iter.getUuid().equals(charUuid)) {
+ Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected.");
+ break;
+ }
+ }
+
+ if (foundChar == null) {
+ Log.w(TAG, "writeCharacteristic: update for unknown characteristic failed");
+ return false;
+ }
+
+ // User may have set minimum and/or maximum characteristic value size. Enforce
+ // them here. If the user has not defined these limits, the default values 0..INT_MAX
+ // do not limit anything.
+ final int minValueLength = ((QtBluetoothGattCharacteristic)foundChar).minValueLength;
+ final int maxValueLength = ((QtBluetoothGattCharacteristic)foundChar).maxValueLength;
+ if (newValue.length < minValueLength || newValue.length > maxValueLength) {
+ Log.w(TAG, "writeCharacteristic: invalid value length: "
+ + newValue.length + ", min: " + minValueLength + ", max: " + maxValueLength);
+ return false;
+ }
+
+ synchronized (this) // a value update might be in progress
+ {
+ ((QtBluetoothGattCharacteristic)foundChar).setLocalValue(newValue);
+ // Value is updated even if server is not connected, but notifying is not possible
+ if (mGattServer != null)
+ sendNotificationsOrIndications(foundChar);
+ }
+
+ return true;
+ }
+
+ /*
+ Updates the local database value for the given \a descUuid to \a newValue.
+
+ This function is called from the Qt thread.
+ */
+ boolean writeDescriptor(BluetoothGattService service, UUID charUuid, UUID descUuid,
+ byte[] newValue)
+ {
+ BluetoothGattDescriptor foundDesc = null;
+ BluetoothGattCharacteristic foundChar = null;
+ final List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
+ for (BluetoothGattCharacteristic iter: charList) {
+ if (!iter.getUuid().equals(charUuid))
+ continue;
+
+ if (foundChar == null) {
+ foundChar = iter;
+ } else {
+ Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected.");
+ break;
+ }
+ }
+
+ if (foundChar != null)
+ foundDesc = foundChar.getDescriptor(descUuid);
+
+ if (foundChar == null || foundDesc == null) {
+ Log.w(TAG, "writeDescriptor: update for unknown char or desc failed (" + foundChar + ")");
+ return false;
+ }
+
+ // we even write CLIENT_CHARACTERISTIC_CONFIGURATION_UUID this way as we choose
+ // to interpret the server's call as a change of the default value.
+ synchronized (this) // a value update might be in progress
+ {
+ ((QtBluetoothGattDescriptor)foundDesc).setLocalValue(newValue);
+ }
+
+ return true;
+ }
+
+ /*
+ * Call back handler for Advertisement requests.
+ */
+ private AdvertiseCallback mAdvertiseListener = new AdvertiseCallback()
+ {
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ Log.e(TAG, "Advertising failure: " + errorCode);
+ super.onStartFailure(errorCode);
+
+ // changing errorCode here implies changes to errorCode handling on Qt side
+ int qtErrorCode = 0;
+ switch (errorCode) {
+ case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
+ return; // ignore -> noop
+ case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
+ Log.e(TAG, "Please reduce size of advertising data.");
+ qtErrorCode = 1;
+ break;
+ case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
+ qtErrorCode = 2;
+ break;
+ default: // default maps to internal error
+ case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
+ qtErrorCode = 3;
+ break;
+ case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
+ qtErrorCode = 4;
+ break;
+ }
+
+ if (qtErrorCode > 0)
+ leServerAdvertisementError(qtObject, qtErrorCode);
+ }
+ };
+
+ native void leConnectionStateChange(long qtObject, int errorCode, int newState);
+ native void leMtuChanged(long qtObject, int mtu);
+ native void leServerAdvertisementError(long qtObject, int status);
+ native void leServerCharacteristicChanged(long qtObject,
+ BluetoothGattCharacteristic characteristic,
+ byte[] newValue);
+ native void leServerDescriptorWritten(long qtObject,
+ BluetoothGattDescriptor descriptor,
+ byte[] newValue);
+}
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothSocketServer.java b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothSocketServer.java
index a10b1f62..cc96eb31 100644
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothSocketServer.java
+++ b/src/android/bluetooth/src/org/qtproject/qt/android/bluetooth/QtBluetoothSocketServer.java
@@ -1,60 +1,28 @@
-/****************************************************************************
-**
-** 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$
-**
-****************************************************************************/
-
-package org.qtproject.qt5.android.bluetooth;
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothSocket;
+import android.content.Context;
import android.util.Log;
import java.io.IOException;
import java.util.UUID;
@SuppressWarnings("WeakerAccess")
-public class QtBluetoothSocketServer extends Thread
+class QtBluetoothSocketServer extends Thread
{
/* Pointer to the Qt object that "owns" the Java object */
@SuppressWarnings({"WeakerAccess", "CanBeFinal"})
long qtObject = 0;
@SuppressWarnings({"WeakerAccess", "CanBeFinal"})
- public boolean logEnabled = false;
+ boolean logEnabled = false;
+ @SuppressWarnings("WeakerAccess")
+ static Context qtContext = null;
private static final String TAG = "QtBluetooth";
private boolean m_isSecure = false;
@@ -67,12 +35,13 @@ public class QtBluetoothSocketServer extends Thread
private static final int QT_LISTEN_FAILED = 1;
private static final int QT_ACCEPT_FAILED = 2;
- public QtBluetoothSocketServer()
+ QtBluetoothSocketServer(Context context)
{
+ qtContext = context;
setName("QtSocketServerThread");
}
- public void setServiceDetails(String uuid, String serviceName, boolean isSecure)
+ void setServiceDetails(String uuid, String serviceName, boolean isSecure)
{
m_uuid = UUID.fromString(uuid);
m_serviceName = serviceName;
@@ -80,9 +49,18 @@ public class QtBluetoothSocketServer extends Thread
}
+ @Override
public void run()
{
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ BluetoothManager manager =
+ (BluetoothManager)qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
+
+ if (manager == null) {
+ errorOccurred(qtObject, QT_NO_BLUETOOTH_SUPPORTED);
+ return;
+ }
+
+ BluetoothAdapter adapter = manager.getAdapter();
if (adapter == null) {
errorOccurred(qtObject, QT_NO_BLUETOOTH_SUPPORTED);
return;
@@ -106,6 +84,9 @@ public class QtBluetoothSocketServer extends Thread
return;
}
+ if (isInterrupted()) // close() may have been called
+ return;
+
BluetoothSocket s;
if (m_serverSocket != null) {
try {
@@ -131,7 +112,25 @@ public class QtBluetoothSocketServer extends Thread
Log.d(TAG, "Leaving server socket thread.");
}
- public void close()
+ // This function closes the socket server
+ //
+ // A note on threading behavior
+ // 1. This function is called from Qt thread which is different from the Java thread (run())
+ // 2. The caller of this function expects the Java thread to be finished upon return
+ //
+ // First we mark the Java thread as interrupted, then call close() on the
+ // listening socket if it had been created, and lastly wait for the thread to finish.
+ // The close() method of the socket is intended to be used to abort the accept() from
+ // another thread, as per the accept() documentation.
+ //
+ // If the Java thread was in the middle of creating a socket with the non-blocking
+ // listen* call, the run() will notice after the returning from the listen* that it has
+ // been interrupted and returns early from the run().
+ //
+ // If the Java thread was in the middle of the blocking accept() call, it will get
+ // interrupated by the close() call on the socket. After returning the run() will
+ // notice it has been interrupted and return from the run()
+ void close()
{
if (!isAlive())
return;
@@ -143,12 +142,14 @@ public class QtBluetoothSocketServer extends Thread
//interrupts accept() call above
if (m_serverSocket != null)
m_serverSocket.close();
- } catch (IOException ex) {
+ // Wait for the thread to finish
+ join(20); // Maximum wait in ms, typically takes < 1ms
+ } catch (Exception ex) {
Log.d(TAG, "Closing server socket close() failed:" + ex.toString());
ex.printStackTrace();
}
}
- public static native void errorOccurred(long qtObject, int errorCode);
- public static native void newSocket(long qtObject, BluetoothSocket socket);
+ static native void errorOccurred(long qtObject, int errorCode);
+ static native void newSocket(long qtObject, BluetoothSocket socket);
}
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothBroadcastReceiver.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothBroadcastReceiver.java
deleted file mode 100644
index 6b46ec0a..00000000
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothBroadcastReceiver.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2016 Lauri Laanmets (Proekspert AS) <lauri.laanmets@eesti.ee>
-** 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$
-**
-****************************************************************************/
-
-package org.qtproject.qt5.android.bluetooth;
-
-import android.app.Activity;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.HashSet;
-import java.util.List;
-
-public class QtBluetoothBroadcastReceiver extends BroadcastReceiver
-{
- /* Pointer to the Qt object that "owns" the Java object */
- @SuppressWarnings("WeakerAccess")
- long qtObject = 0;
- @SuppressWarnings("WeakerAccess")
- static Context qtContext = null;
-
- private static final int TURN_BT_ON = 3330;
- private static final int TURN_BT_DISCOVERABLE = 3331;
- private static final String TAG = "QtBluetoothBroadcastReceiver";
-
- public void onReceive(Context context, Intent intent)
- {
- synchronized (qtContext) {
- if (qtObject == 0)
- return;
-
- jniOnReceive(qtObject, context, intent);
- }
- }
-
- public void unregisterReceiver()
- {
- synchronized (qtContext) {
- qtObject = 0;
- qtContext.unregisterReceiver(this);
- }
- }
-
- public native void jniOnReceive(long qtObject, Context context, Intent intent);
-
- static public void setContext(Context context)
- {
- qtContext = context;
- }
-
- static public void setDiscoverable()
- {
- if (!(qtContext instanceof android.app.Activity)) {
- Log.w(TAG, "Discovery mode cannot be enabled from a service.");
- return;
- }
-
- Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
- intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
- try {
- ((Activity)qtContext).startActivityForResult(intent, TURN_BT_ON);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
-
- static public void setConnectable()
- {
- if (!(qtContext instanceof android.app.Activity)) {
- Log.w(TAG, "Connectable mode cannot be enabled from a service.");
- return;
- }
-
- Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
- try {
- ((Activity)qtContext).startActivityForResult(intent, TURN_BT_DISCOVERABLE);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
-
- static public boolean setPairingMode(String address, boolean isPairing)
- {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- try {
- BluetoothDevice device = adapter.getRemoteDevice(address);
- String methodName = "createBond";
- if (!isPairing)
- methodName = "removeBond";
-
- Method m = device.getClass()
- .getMethod(methodName, (Class[]) null);
- m.invoke(device, (Object[]) null);
- } catch (Exception ex) {
- ex.printStackTrace();
- return false;
- }
-
- return true;
- }
-
- /*
- * Returns a list of remote devices confirmed to be connected.
- *
- * This list is not complete as it only detects GATT/BtLE related connections.
- * Unfortunately there is no API that provides the complete list.
- *
- * The function uses Android API v11 & v18. We need to use reflection.
- */
- static public String[] getConnectedDevices()
- {
- try {
- //Bluetooth service name
- Field f = Context.class.getField("BLUETOOTH_SERVICE");
- String serviceValueString = (String)f.get(qtContext);
-
- Class btProfileClz = Class.forName("android.bluetooth.BluetoothProfile");
-
- //value of BluetoothProfile.GATT
- f = btProfileClz.getField("GATT");
- int gatt = f.getInt(null);
-
- //value of BluetoothProfile.GATT_SERVER
- f = btProfileClz.getField("GATT_SERVER");
- int gattServer = f.getInt(null);
-
- //get BluetoothManager instance
- Object bluetoothManager = qtContext.getSystemService(serviceValueString);
-
- Class[] cArg = new Class[1];
- cArg[0] = int.class;
- Method m = bluetoothManager.getClass().getMethod("getConnectedDevices", cArg);
-
- List gattConnections = (List) m.invoke(bluetoothManager, gatt);
- List gattServerConnections = (List) m.invoke(bluetoothManager, gattServer);
-
- //process found remote connections but avoid duplications
- HashSet<String> set = new HashSet<String>();
- for (Object gattConnection : gattConnections)
- set.add(gattConnection.toString());
-
- for (Object gattServerConnection : gattServerConnections)
- set.add(gattServerConnection.toString());
-
- return set.toArray(new String[set.size()]);
- } catch (Exception ex) {
- //API is less than 18
- return new String[0];
- }
- }
-}
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothInputStreamThread.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothInputStreamThread.java
deleted file mode 100644
index 068febda..00000000
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothInputStreamThread.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/****************************************************************************
-**
-** 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$
-**
-****************************************************************************/
-
-package org.qtproject.qt5.android.bluetooth;
-
-import java.io.InputStream;
-import java.io.IOException;
-import android.util.Log;
-
-@SuppressWarnings("WeakerAccess")
-public class QtBluetoothInputStreamThread extends Thread
-{
- /* Pointer to the Qt object that "owns" the Java object */
- @SuppressWarnings("CanBeFinal")
- long qtObject = 0;
- @SuppressWarnings("CanBeFinal")
- public boolean logEnabled = false;
- private static final String TAG = "QtBluetooth";
- private InputStream m_inputStream = null;
-
- //error codes
- public static final int QT_MISSING_INPUT_STREAM = 0;
- public static final int QT_READ_FAILED = 1;
- public static final int QT_THREAD_INTERRUPTED = 2;
-
- public QtBluetoothInputStreamThread()
- {
- setName("QtBtInputStreamThread");
- }
-
- public void setInputStream(InputStream stream)
- {
- m_inputStream = stream;
- }
-
- public void run()
- {
- if (m_inputStream == null) {
- errorOccurred(qtObject, QT_MISSING_INPUT_STREAM);
- return;
- }
-
- byte[] buffer = new byte[1000];
- int bytesRead;
-
- try {
- while (!isInterrupted()) {
- //this blocks until we see incoming data
- //or close() on related BluetoothSocket is called
- bytesRead = m_inputStream.read(buffer);
- readyData(qtObject, buffer, bytesRead);
- }
-
- errorOccurred(qtObject, QT_THREAD_INTERRUPTED);
- } catch (IOException ex) {
- if (logEnabled)
- Log.d(TAG, "InputStream.read() failed:" + ex.toString());
- ex.printStackTrace();
- errorOccurred(qtObject, QT_READ_FAILED);
- }
-
- if (logEnabled)
- Log.d(TAG, "Leaving input stream thread");
- }
-
- public static native void errorOccurred(long qtObject, int errorCode);
- public static native void readyData(long qtObject, byte[] buffer, int bufferLength);
-}
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
deleted file mode 100644
index 8a69b4c7..00000000
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java
+++ /dev/null
@@ -1,1532 +0,0 @@
-/****************************************************************************
- **
- ** Copyright (C) 2019 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$
- **
- ****************************************************************************/
-
-package org.qtproject.qt5.android.bluetooth;
-
-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.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.Build;
-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;
- private boolean mLeScanRunning = false;
-
- private BluetoothGatt mBluetoothGatt = null;
- private String mRemoteGattAddress;
- private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
- private final int MAX_MTU = 512;
- private final int DEFAULT_MTU = 23;
- private int mSupportedMtu = -1;
-
- /*
- * 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.
- */
- // handle values above zero are for regular handle specific read/write requests
- // handle values below zero are reserved for handle-independent requests
- private int HANDLE_FOR_RESET = -1;
- private int HANDLE_FOR_MTU_EXCHANGE = -2;
- private AtomicInteger handleForTimeout = new AtomicInteger(HANDLE_FOR_RESET); // implies not running by default
-
- private final int RUNNABLE_TIMEOUT = 3000; // 3 seconds
- private final Handler timeoutHandler = new Handler(Looper.getMainLooper());
-
- /* New BTLE scanner setup since Android SDK v21 */
- private BluetoothLeScanner mBluetoothLeScanner = null;
-
- private class TimeoutRunnable implements Runnable {
- public TimeoutRunnable(int handle) { pendingJobHandle = handle; }
- @Override
- public void run() {
- boolean timeoutStillValid = handleForTimeout.compareAndSet(pendingJobHandle, HANDLE_FOR_RESET);
- if (timeoutStillValid) {
- Log.w(TAG, "****** Timeout for request on handle " + (pendingJobHandle & 0xffff));
- Log.w(TAG, "****** Looks like the peripheral does NOT act in " +
- "accordance to Bluetooth 4.x spec.");
- Log.w(TAG, "****** Please check server implementation. Continuing under " +
- "reservation.");
-
- if (pendingJobHandle > HANDLE_FOR_RESET)
- interruptCurrentIO(pendingJobHandle & 0xffff);
- else if (pendingJobHandle < HANDLE_FOR_RESET)
- interruptCurrentIO(pendingJobHandle);
- }
- }
-
- // 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")
- Context qtContext = null;
-
- @SuppressWarnings("WeakerAccess")
- public QtBluetoothLE() {
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
- }
-
- public QtBluetoothLE(final String remoteAddress, Context context) {
- this();
- qtContext = context;
- mRemoteGattAddress = remoteAddress;
- }
-
- /*************************************************************/
- /* Device scan */
- /*************************************************************/
-
- /*
- Returns true, if request was successfully completed
- */
- public boolean scanForLeDevice(final boolean isEnabled) {
- if (isEnabled == mLeScanRunning)
- return true;
-
- if (isEnabled) {
- Log.d(TAG, "New BTLE scanning API");
- ScanSettings.Builder settingsBuilder = new ScanSettings.Builder();
- settingsBuilder = settingsBuilder.setScanMode(ScanSettings.SCAN_MODE_BALANCED);
- ScanSettings settings = settingsBuilder.build();
-
- List<ScanFilter> filterList = new ArrayList<ScanFilter>(2);
-
- mBluetoothLeScanner.startScan(filterList, settings, leScanCallback21);
- mLeScanRunning = true;
- } else {
- mBluetoothLeScanner.stopScan(leScanCallback21);
- mLeScanRunning = false;
- }
-
- return (mLeScanRunning == isEnabled);
- }
-
- // Device scan callback (SDK v21+)
- private final ScanCallback leScanCallback21 = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- super.onScanResult(callbackType, result);
- leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
- }
-
- @Override
- public void onBatchScanResults(List<ScanResult> results) {
- super.onBatchScanResults(results);
- for (ScanResult result : results)
- leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
-
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- super.onScanFailed(errorCode);
- Log.d(TAG, "BTLE device scan failed with " + errorCode);
- }
- };
-
- public native void leScanResult(long qtObject, BluetoothDevice device, int rssi, byte[] scanRecord);
-
- /*************************************************************/
- /* Service Discovery */
- /*************************************************************/
-
- private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
-
- public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
- if (qtObject == 0)
- return;
-
- int qLowEnergyController_State = 0;
- //This must be in sync with QLowEnergyController::ControllerState
- switch (newState) {
- case BluetoothProfile.STATE_DISCONNECTED:
- 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.
- if (mBluetoothGatt != null)
- mBluetoothGatt.close();
- mBluetoothGatt = null;
- break;
- case BluetoothProfile.STATE_CONNECTED:
- qLowEnergyController_State = 2;
- }
-
- //This must be in sync with QLowEnergyController::Error
- int errorCode;
- switch (status) {
- case BluetoothGatt.GATT_SUCCESS:
- errorCode = 0; break; //QLowEnergyController::NoError
- case BluetoothGatt.GATT_FAILURE: // Android's equivalent of "do not know what error it is"
- errorCode = 1; break; //QLowEnergyController::UnknownError
- case 8: // BLE_HCI_CONNECTION_TIMEOUT
- Log.w(TAG, "Connection Error: Try to delay connect() call after previous activity");
- errorCode = 5; break; //QLowEnergyController::ConnectionError
- case 19: // BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION
- case 20: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES
- case 21: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_POWER_OFF
- Log.w(TAG, "The remote host closed the connection");
- errorCode = 7; //QLowEnergyController::RemoteHostClosedError
- break;
- case 22: // BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION
- // Internally, Android maps PIN_OR_KEY_MISSING to GATT_CONN_TERMINATE_LOCAL_HOST
- errorCode = 8; break; //QLowEnergyController::AuthorizationError
- default:
- Log.w(TAG, "Unhandled error code on connectionStateChanged: " + status + " " + newState);
- errorCode = status; break; //TODO deal with all errors
- }
- leConnectionStateChange(qtObject, errorCode, qLowEnergyController_State);
- }
-
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
- //This must be in sync with QLowEnergyController::Error
- int errorCode;
- StringBuilder builder = new StringBuilder();
- switch (status) {
- case BluetoothGatt.GATT_SUCCESS:
- errorCode = 0; //QLowEnergyController::NoError
- final List<BluetoothGattService> services = mBluetoothGatt.getServices();
- for (BluetoothGattService service: services) {
- builder.append(service.getUuid().toString()).append(" "); //space is separator
- }
- break;
- default:
- Log.w(TAG, "Unhandled error code on onServicesDiscovered: " + status);
- errorCode = status; break; //TODO deal with all errors
- }
- leServicesDiscovered(qtObject, errorCode, builder.toString());
-
- scheduleMtuExchange();
- }
-
- public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
- android.bluetooth.BluetoothGattCharacteristic characteristic,
- int 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;
- }
-
- performNextIO();
- return;
- }
- }
-
- boolean requestTimedOut = !handleForTimeout.compareAndSet(
- modifiedReadWriteHandle(foundHandle, IoJobType.Read), HANDLE_FOR_RESET);
- 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;
- }
-
- GattEntry entry = entries.get(foundHandle);
- final boolean isServiceDiscoveryRun = !entry.valueKnown;
- entry.valueKnown = true;
-
- 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);
- }
- }
-
- 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);
- }
-
- //unlock the queue for next item
- synchronized (readWriteQueue) {
- ioJobPending = false;
- }
-
- performNextIO();
- }
-
- 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;
- }
-
- boolean requestTimedOut = !handleForTimeout.compareAndSet(
- modifiedReadWriteHandle(handle, IoJobType.Write), HANDLE_FOR_RESET);
- 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) {
- 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)
- {
- int foundHandle = -1;
- synchronized (this) {
- 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), HANDLE_FOR_RESET);
- 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;
- }
-
- GattEntry entry = entries.get(foundHandle);
- final boolean isServiceDiscoveryRun = !entry.valueKnown;
- entry.valueKnown = true;
-
- 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) {
- // Cannot read but still advertise the fact that we found a descriptor
- // The value will be empty.
- Log.w(TAG, "onDescriptorRead during discovery error: " + status);
- Log.d(TAG, "Non-readable descriptor " + descriptor.getUuid() +
- " for characteristic " + descriptor.getCharacteristic().getUuid() +
- " for service " + descriptor.getCharacteristic().getService().getUuid());
- 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);
- }
-
- }
-
- 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);
- }
-
- /* 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);
- }
- }
- }
-
- //unlock the queue for next item
- synchronized (readWriteQueue) {
- ioJobPending = false;
- }
-
- performNextIO();
- }
-
- 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);
-
- boolean requestTimedOut = !handleForTimeout.compareAndSet(
- modifiedReadWriteHandle(handle, IoJobType.Write), HANDLE_FOR_RESET);
- 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) {
- 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");
-// }
-
- // requires Android API v21
- public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status)
- {
- if (status == BluetoothGatt.GATT_SUCCESS) {
- Log.w(TAG, "MTU changed to " + mtu);
- mSupportedMtu = mtu;
- } else {
- Log.w(TAG, "MTU change error " + status + ". New MTU " + mtu);
- mSupportedMtu = DEFAULT_MTU;
- }
-
- boolean requestTimedOut = !handleForTimeout.compareAndSet(
- modifiedReadWriteHandle(HANDLE_FOR_MTU_EXCHANGE, IoJobType.Mtu), HANDLE_FOR_RESET);
- if (requestTimedOut) {
- Log.w(TAG, "Late mtu reply after timeout was hit");
- // Timeout has hit before this response -> ignore the response
- // no need to unlock ioJobPending -> the timeout has done that already
- return;
- }
-
- synchronized (readWriteQueue) {
- ioJobPending = false;
- }
-
- performNextIO();
- }
- };
-
-
- public boolean connect() {
- BluetoothDevice mRemoteGattDevice;
-
- try {
- mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
- } catch (IllegalArgumentException ex) {
- Log.w(TAG, "Remote address is not valid: " + mRemoteGattAddress);
- return false;
- }
-
- 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;
- }
-
- public void disconnect() {
- if (mBluetoothGatt == null)
- return;
-
- mBluetoothGatt.disconnect();
- }
-
- 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;
- /*
- * 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
- {
- Read, Write, Mtu
- }
-
- private class ReadWriteJob
- {
- public GattEntry entry;
- public byte[] newValue;
- public int requestedWriteType;
- 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>();
-
-
- 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(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());
- 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;
- 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
- List<BluetoothGattDescriptor> descList = characteristic.getDescriptors();
- for (BluetoothGattDescriptor desc: descList) {
- 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.trimToSize();
- }
-
- private void resetData()
- {
- synchronized (this) {
- uuidToEntry.clear();
- entries.clear();
- servicesToBeDiscovered.clear();
- }
-
- // kill all timeout handlers
- timeoutHandler.removeCallbacksAndMessages(null);
- handleForTimeout.set(HANDLE_FOR_RESET);
-
- 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 discovered or under investigation
- if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) {
- Log.w(TAG, "Service already known or to be discovered");
- return true;
- }
-
- servicesToBeDiscovered.add(serviceHandle);
- scheduleServiceDetailDiscovery(serviceHandle);
- performNextIO();
-
- } 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)
- {
- if (mBluetoothGatt == null)
- return null;
-
- 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();
- }
-
- //TODO function not yet used
- private void finishCurrentServiceDiscovery(int handleDiscoveredService)
- {
- Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService);
- GattEntry discoveredService = entries.get(handleDiscoveredService);
- discoveredService.valueKnown = true;
- synchronized (this) {
- try {
- 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 boolean executeMtuExchange()
- {
- if (Build.VERSION.SDK_INT >= 21) {
- try {
- Method mtuMethod = mBluetoothGatt.getClass().getDeclaredMethod("requestMtu", int.class);
- if (mtuMethod != null) {
- Boolean success = (Boolean) mtuMethod.invoke(mBluetoothGatt, MAX_MTU);
- if (success.booleanValue()) {
- Log.w(TAG, "MTU change initiated");
- return false;
- } else {
- Log.w(TAG, "MTU change request failed");
- }
- }
- } catch (Exception ex) {}
- }
-
- Log.w(TAG, "Assuming default MTU value of 23 bytes");
-
- mSupportedMtu = DEFAULT_MTU;
- return true;
- }
-
- private void scheduleMtuExchange()
- {
- ReadWriteJob newJob = new ReadWriteJob();
- newJob.jobType = IoJobType.Mtu;
- newJob.entry = null;
-
- synchronized (readWriteQueue) {
- readWriteQueue.add(newJob);
- }
-
- performNextIO();
- }
-
- /*
- 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)
- {
- GattEntry serviceEntry = entries.get(serviceHandle);
- final int endHandle = serviceEntry.endHandle;
-
- if (serviceHandle == endHandle) {
- Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover");
- finishCurrentServiceDiscovery(serviceHandle);
- return;
- }
-
- 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
-
-
- // 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);
- }
- }
- }
-
- /*************************************************************/
- /* 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
- 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;
- }
-
- 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;
-
- 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;
- }
-
- // Called by TimeoutRunnable if the current I/O job timed 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();
-
- if (handle == HANDLE_FOR_MTU_EXCHANGE)
- return;
-
- try {
- synchronized (this) {
-
- 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);
- }
- } catch (IndexOutOfBoundsException outOfBounds) {
- Log.w(TAG, "interruptCurrentIO(): Unknown gatt entry, index: "
- + handle + " size: " + entries.size());
- }
- }
-
- /*
- 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;
- int handle = HANDLE_FOR_RESET;
-
- synchronized (readWriteQueue) {
- if (readWriteQueue.isEmpty() || ioJobPending)
- return;
-
- nextJob = readWriteQueue.remove();
- if (nextJob.jobType == IoJobType.Mtu) {
- handle = HANDLE_FOR_MTU_EXCHANGE; //mtu request is special case
- } else {
- 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));
-
- switch (nextJob.jobType) {
- case Read:
- skip = executeReadJob(nextJob);
- break;
- case Write:
- skip = executeWriteJob(nextJob);
- break;
- case Mtu:
- skip = executeMtuExchange();
- break;
- }
-
- if (skip) {
- handleForTimeout.set(HANDLE_FOR_RESET); // not a pending call -> release atomic
- } else {
- ioJobPending = true;
- timeoutHandler.postDelayed(new TimeoutRunnable(
- modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT);
- }
-
- if (nextJob.jobType != IoJobType.Mtu) {
- 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);
- }
- }
-
- 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. The error report is not required during
- the initial service discovery though.
- */
- if (handle > HANDLE_FOR_RESET) {
- // 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 (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)
- break;
- case Service:
- Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen.");
- break;
- }
-
- // last entry of current discovery run?
- synchronized (this) {
- try {
- GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
- if (serviceEntry.endHandle == handle)
- finishCurrentServiceDiscovery(entry.associatedServiceHandle);
- } catch (IndexOutOfBoundsException outOfBounds) {
- Log.w(TAG, "performNextIO(): Unknown service for entry, index: "
- + entry.associatedServiceHandle + " size: " + entries.size());
- }
- }
- } else {
- int errorCode = 0;
-
- // The error codes below must be in sync with QLowEnergyService::ServiceError
- if (nextJob.jobType == IoJobType.Read) {
- errorCode = (entry.type == GattEntryType.Characteristic) ?
- 5 : 6; // CharacteristicReadError : DescriptorReadError
- } else {
- errorCode = (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:
- 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:
- 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:
- 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;
- case Mtu:
- modifiedHandle = HANDLE_FOR_MTU_EXCHANGE;
- 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,
- 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);
-}
-
diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java
deleted file mode 100644
index cdd16686..00000000
--- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLEServer.java
+++ /dev/null
@@ -1,611 +0,0 @@
-/****************************************************************************
- **
- ** 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$
- **
- ****************************************************************************/
-
-package org.qtproject.qt5.android.bluetooth;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattServer;
-import android.bluetooth.BluetoothGattServerCallback;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseData.Builder;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.HashMap;
-import java.util.UUID;
-
-public class QtBluetoothLEServer {
- private static final String TAG = "QtBluetoothGattServer";
-
- /* Pointer to the Qt object that "owns" the Java object */
- @SuppressWarnings({"CanBeFinal", "WeakerAccess"})
- long qtObject = 0;
- @SuppressWarnings("WeakerAccess")
-
- private Context qtContext = null;
-
- // Bluetooth members
- private final BluetoothAdapter mBluetoothAdapter;
- private BluetoothGattServer mGattServer = null;
- private BluetoothLeAdvertiser mLeAdvertiser = null;
-
- private String mRemoteName = "";
- public String remoteName() { return mRemoteName; }
-
- private String mRemoteAddress = "";
- public String remoteAddress() { return mRemoteAddress; }
-
- /*
- As per Bluetooth specification each connected device can have individual and persistent
- Client characteristic configurations (see Bluetooth Spec 5.0 Vol 3 Part G 3.3.3.3)
- This class manages the existing configurrations.
- */
- private class ClientCharacteristicManager {
- private final HashMap<BluetoothGattCharacteristic, List<Entry>> notificationStore = new HashMap<BluetoothGattCharacteristic, List<Entry>>();
-
- private class Entry {
- BluetoothDevice device = null;
- byte[] value = null;
- boolean isConnected = false;
- }
-
- public void insertOrUpdate(BluetoothGattCharacteristic characteristic,
- BluetoothDevice device, byte[] newValue)
- {
- if (notificationStore.containsKey(characteristic)) {
-
- List<Entry> entries = notificationStore.get(characteristic);
- for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).device.equals(device)) {
- Entry e = entries.get(i);
- e.value = newValue;
- entries.set(i, e);
- return;
- }
- }
-
- // not match so far -> add device to list
- Entry e = new Entry();
- e.device = device;
- e.value = newValue;
- e.isConnected = true;
- entries.add(e);
- return;
- }
-
- // new characteristic
- Entry e = new Entry();
- e.device = device;
- e.value = newValue;
- e.isConnected = true;
- List<Entry> list = new LinkedList<Entry>();
- list.add(e);
- notificationStore.put(characteristic, list);
- }
-
- /*
- Marks client characteristic configuration entries as (in)active based the associated
- devices general connectivity state.
- This function avoids that existing configurations are not acted
- upon when the associated device is not connected.
- */
- public void markDeviceConnectivity(BluetoothDevice device, boolean isConnected)
- {
- final Iterator<BluetoothGattCharacteristic> keys = notificationStore.keySet().iterator();
- while (keys.hasNext()) {
- final BluetoothGattCharacteristic characteristic = keys.next();
- final List<Entry> entries = notificationStore.get(characteristic);
- if (entries == null)
- continue;
-
- ListIterator<Entry> charConfig = entries.listIterator();
- while (charConfig.hasNext()) {
- Entry e = charConfig.next();
- if (e.device.equals(device))
- e.isConnected = isConnected;
- }
- }
- }
-
- // Returns list of all BluetoothDevices which require notification or indication.
- // No match returns an empty list.
- List<BluetoothDevice> getToBeUpdatedDevices(BluetoothGattCharacteristic characteristic)
- {
- ArrayList<BluetoothDevice> result = new ArrayList<BluetoothDevice>();
- if (!notificationStore.containsKey(characteristic))
- return result;
-
- final ListIterator<Entry> iter = notificationStore.get(characteristic).listIterator();
- while (iter.hasNext())
- result.add(iter.next().device);
-
- return result;
- }
-
- // Returns null if no match; otherwise the configured actual client characteristic
- // configuration value
- byte[] valueFor(BluetoothGattCharacteristic characteristic, BluetoothDevice device)
- {
- if (!notificationStore.containsKey(characteristic))
- return null;
-
- List<Entry> entries = notificationStore.get(characteristic);
- for (int i = 0; i < entries.size(); i++) {
- final Entry entry = entries.get(i);
- if (entry.device.equals(device) && entry.isConnected == true)
- return entries.get(i).value;
- }
-
- return null;
- }
- }
-
- private static final UUID CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = UUID
- .fromString("00002902-0000-1000-8000-00805f9b34fb");
- ClientCharacteristicManager clientCharacteristicManager = new ClientCharacteristicManager();
-
- public QtBluetoothLEServer(Context context)
- {
- qtContext = context;
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-
- if (mBluetoothAdapter == null || qtContext == null) {
- Log.w(TAG, "Missing Bluetooth adapter or Qt context. Peripheral role disabled.");
- return;
- }
-
- BluetoothManager manager = (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
- if (manager == null) {
- Log.w(TAG, "Bluetooth service not available.");
- return;
- }
-
- mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
-
- if (!mBluetoothAdapter.isMultipleAdvertisementSupported())
- Log.w(TAG, "Device does not support Bluetooth Low Energy advertisement.");
- else
- Log.w(TAG, "Let's do BTLE Peripheral.");
- }
-
- /*
- * Call back handler for the Gatt Server.
- */
- private BluetoothGattServerCallback mGattServerListener = new BluetoothGattServerCallback()
- {
- @Override
- public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
- Log.w(TAG, "Our gatt server connection state changed, new state: " + newState + " " + status);
- super.onConnectionStateChange(device, status, newState);
-
- int qtControllerState = 0;
- switch (newState) {
- case BluetoothProfile.STATE_DISCONNECTED:
- qtControllerState = 0; // QLowEnergyController::UnconnectedState
- clientCharacteristicManager.markDeviceConnectivity(device, false);
- mGattServer.close();
- break;
- case BluetoothProfile.STATE_CONNECTED:
- clientCharacteristicManager.markDeviceConnectivity(device, true);
- qtControllerState = 2; // QLowEnergyController::ConnectedState
- break;
- }
-
- mRemoteName = device.getName();
- mRemoteAddress = device.getAddress();
-
- int qtErrorCode;
- switch (status) {
- case BluetoothGatt.GATT_SUCCESS:
- qtErrorCode = 0; break;
- default:
- Log.w(TAG, "Unhandled error code on peripheral connectionStateChanged: " + status + " " + newState);
- qtErrorCode = status;
- break;
- }
-
- leServerConnectionStateChange(qtObject, qtErrorCode, qtControllerState);
- }
-
- @Override
- public void onServiceAdded(int status, BluetoothGattService service) {
- super.onServiceAdded(status, service);
- }
-
- @Override
- public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic)
- {
- byte[] dataArray;
- try {
- dataArray = Arrays.copyOfRange(characteristic.getValue(), offset, characteristic.getValue().length);
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, dataArray);
- } catch (Exception ex) {
- Log.w(TAG, "onCharacteristicReadRequest: " + requestId + " " + offset + " " + characteristic.getValue().length);
- ex.printStackTrace();
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
- }
-
- super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
- }
-
- @Override
- public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic,
- boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)
- {
- Log.w(TAG, "onCharacteristicWriteRequest");
- int resultStatus = BluetoothGatt.GATT_SUCCESS;
- boolean sendNotificationOrIndication = false;
- if (!preparedWrite) { // regular write
- if (offset == 0) {
- characteristic.setValue(value);
- leServerCharacteristicChanged(qtObject, characteristic, value);
- sendNotificationOrIndication = true;
- } else {
- // This should not really happen as per Bluetooth spec
- Log.w(TAG, "onCharacteristicWriteRequest: !preparedWrite, offset " + offset + ", Not supported");
- resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
- }
-
-
- } else {
- Log.w(TAG, "onCharacteristicWriteRequest: preparedWrite, offset " + offset + ", Not supported");
- resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
-
- // TODO we need to record all requests and execute them in one go once onExecuteWrite() is received
- // we use a queue to remember the pending requests
- // TODO we are ignoring the device identificator for now -> Bluetooth spec requires a queue per device
- }
-
-
- if (responseNeeded)
- mGattServer.sendResponse(device, requestId, resultStatus, offset, value);
- if (sendNotificationOrIndication)
- sendNotificationsOrIndications(characteristic);
-
- super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
- }
-
- @Override
- public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor)
- {
- byte[] dataArray = descriptor.getValue();
- try {
- if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
- dataArray = clientCharacteristicManager.valueFor(descriptor.getCharacteristic(), device);
- if (dataArray == null)
- dataArray = descriptor.getValue();
- }
-
- dataArray = Arrays.copyOfRange(dataArray, offset, dataArray.length);
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, dataArray);
- } catch (Exception ex) {
- Log.w(TAG, "onDescriptorReadRequest: " + requestId + " " + offset + " " + dataArray.length);
- ex.printStackTrace();
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, offset, null);
- }
-
- super.onDescriptorReadRequest(device, requestId, offset, descriptor);
- }
-
- @Override
- public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor,
- boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)
- {
- int resultStatus = BluetoothGatt.GATT_SUCCESS;
- if (!preparedWrite) { // regular write
- if (offset == 0) {
- descriptor.setValue(value);
-
- if (descriptor.getUuid().equals(CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)) {
- clientCharacteristicManager.insertOrUpdate(descriptor.getCharacteristic(),
- device, value);
- }
-
- leServerDescriptorWritten(qtObject, descriptor, value);
- } else {
- // This should not really happen as per Bluetooth spec
- Log.w(TAG, "onDescriptorWriteRequest: !preparedWrite, offset " + offset + ", Not supported");
- resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
- }
-
-
- } else {
- Log.w(TAG, "onDescriptorWriteRequest: preparedWrite, offset " + offset + ", Not supported");
- resultStatus = BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED;
- // TODO we need to record all requests and execute them in one go once onExecuteWrite() is received
- // we use a queue to remember the pending requests
- // TODO we are ignoring the device identificator for now -> Bluetooth spec requires a queue per device
- }
-
-
- if (responseNeeded)
- mGattServer.sendResponse(device, requestId, resultStatus, offset, value);
-
- super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
- }
-
- @Override
- public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute)
- {
- // TODO not yet implemented -> return proper GATT error for it
- mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED, 0, null);
-
- super.onExecuteWrite(device, requestId, execute);
- }
-
- @Override
- public void onNotificationSent(BluetoothDevice device, int status) {
- super.onNotificationSent(device, status);
- Log.w(TAG, "onNotificationSent" + device + " " + status);
- }
-
- // MTU change disabled since it requires API level 22. Right now we only enforce lvl 21
-// @Override
-// public void onMtuChanged(BluetoothDevice device, int mtu) {
-// super.onMtuChanged(device, mtu);
-// }
- };
-
- public boolean connectServer()
- {
- if (mGattServer != null)
- return true;
-
- BluetoothManager manager = (BluetoothManager) qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
- if (manager == null) {
- Log.w(TAG, "Bluetooth service not available.");
- return false;
- }
-
- mGattServer = manager.openGattServer(qtContext, mGattServerListener);
-
- return (mGattServer != null);
- }
-
- public void disconnectServer()
- {
- if (mGattServer == null)
- return;
-
- mGattServer.close();
- mGattServer = null;
-
- mRemoteName = mRemoteAddress = "";
- leServerConnectionStateChange(qtObject, 0 /*NoError*/, 0 /*QLowEnergyController::UnconnectedState*/);
- }
-
- public boolean startAdvertising(AdvertiseData advertiseData,
- AdvertiseData scanResponse,
- AdvertiseSettings settings)
- {
- if (mLeAdvertiser == null)
- return false;
-
- if (!connectServer()) {
- Log.w(TAG, "Server::startAdvertising: Cannot open GATT server");
- return false;
- }
-
- Log.w(TAG, "Starting to advertise.");
- mLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseListener);
-
- return true;
- }
-
- public void stopAdvertising()
- {
- if (mLeAdvertiser == null)
- return;
-
- mLeAdvertiser.stopAdvertising(mAdvertiseListener);
- Log.w(TAG, "Advertisement stopped.");
- }
-
- public void addService(BluetoothGattService service)
- {
- if (!connectServer()) {
- Log.w(TAG, "Server::addService: Cannot open GATT server");
- return;
- }
-
- mGattServer.addService(service);
- }
-
- /*
- Check the client characteristics configuration for the given characteristic
- and sends notifications or indications as per required.
- */
- private void sendNotificationsOrIndications(BluetoothGattCharacteristic characteristic)
- {
- final ListIterator<BluetoothDevice> iter =
- clientCharacteristicManager.getToBeUpdatedDevices(characteristic).listIterator();
-
- // TODO This quick loop over multiple devices should be synced with onNotificationSent().
- // The next notifyCharacteristicChanged() call must wait until onNotificationSent()
- // was received. At this becomes an issue when the server accepts multiple remote
- // devices at the same time.
- while (iter.hasNext()) {
- final BluetoothDevice device = iter.next();
- final byte[] clientCharacteristicConfig = clientCharacteristicManager.valueFor(characteristic, device);
- if (clientCharacteristicConfig != null) {
- if (Arrays.equals(clientCharacteristicConfig, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
- mGattServer.notifyCharacteristicChanged(device, characteristic, false);
- } else if (Arrays.equals(clientCharacteristicConfig, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)) {
- mGattServer.notifyCharacteristicChanged(device, characteristic, true);
- }
- }
- }
- }
-
- /*
- Updates the local database value for the given characteristic with \a charUuid and
- \a newValue. If notifications for this task are enabled an approproiate notification will
- be send to the remote client.
-
- This function is called from the Qt thread.
- */
- public boolean writeCharacteristic(BluetoothGattService service, UUID charUuid, byte[] newValue)
- {
- BluetoothGattCharacteristic foundChar = null;
- List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
- for (BluetoothGattCharacteristic iter: charList) {
- if (iter.getUuid().equals(charUuid) && foundChar == null) {
- foundChar = iter;
- // don't break here since we want to check next condition below on next iteration
- } else if (iter.getUuid().equals(charUuid)) {
- Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected.");
- break;
- }
- }
-
- if (foundChar == null) {
- Log.w(TAG, "writeCharacteristic: update for unknown characteristic failed");
- return false;
- }
-
- foundChar.setValue(newValue);
- sendNotificationsOrIndications(foundChar);
-
- return true;
- }
-
- /*
- Updates the local database value for the given \a descUuid to \a newValue.
-
- This function is called from the Qt thread.
- */
- public boolean writeDescriptor(BluetoothGattService service, UUID charUuid, UUID descUuid,
- byte[] newValue)
- {
- BluetoothGattDescriptor foundDesc = null;
- BluetoothGattCharacteristic foundChar = null;
- final List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
- for (BluetoothGattCharacteristic iter: charList) {
- if (!iter.getUuid().equals(charUuid))
- continue;
-
- if (foundChar == null) {
- foundChar = iter;
- } else {
- Log.w(TAG, "Found second char with same UUID. Wrong char may have been selected.");
- break;
- }
- }
-
- if (foundChar != null)
- foundDesc = foundChar.getDescriptor(descUuid);
-
- if (foundChar == null || foundDesc == null) {
- Log.w(TAG, "writeDescriptor: update for unknown char or desc failed (" + foundChar + ")");
- return false;
- }
-
- // we even write CLIENT_CHARACTERISTIC_CONFIGURATION_UUID this way as we choose
- // to interpret the server's call as a change of the default value.
- foundDesc.setValue(newValue);
-
- return true;
- }
-
- /*
- * Call back handler for Advertisement requests.
- */
- private AdvertiseCallback mAdvertiseListener = new AdvertiseCallback()
- {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- Log.e(TAG, "Advertising failure: " + errorCode);
- super.onStartFailure(errorCode);
-
- // changing errorCode here implies changes to errorCode handling on Qt side
- int qtErrorCode = 0;
- switch (errorCode) {
- case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
- return; // ignore -> noop
- case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
- qtErrorCode = 1;
- break;
- case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
- qtErrorCode = 2;
- break;
- default: // default maps to internal error
- case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
- qtErrorCode = 3;
- break;
- case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
- qtErrorCode = 4;
- break;
- }
-
- if (qtErrorCode > 0)
- leServerAdvertisementError(qtObject, qtErrorCode);
- }
- };
-
- public native void leServerConnectionStateChange(long qtObject, int errorCode, int newState);
- public native void leServerAdvertisementError(long qtObject, int status);
- public native void leServerCharacteristicChanged(long qtObject,
- BluetoothGattCharacteristic characteristic,
- byte[] newValue);
- public native void leServerDescriptorWritten(long qtObject,
- BluetoothGattDescriptor descriptor,
- byte[] newValue);
-}
diff --git a/src/android/nfc/AndroidManifest.xml b/src/android/nfc/AndroidManifest.xml
index 2ba062ae..30db61fd 100644
--- a/src/android/nfc/AndroidManifest.xml
+++ b/src/android/nfc/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="org.qtproject.qt5.android.nfc"
+ package="org.qtproject.qt.android.nfc"
android:versionCode="1"
android:versionName="1.0">
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
diff --git a/src/android/nfc/CMakeLists.txt b/src/android/nfc/CMakeLists.txt
new file mode 100644
index 00000000..f16f60c4
--- /dev/null
+++ b/src/android/nfc/CMakeLists.txt
@@ -0,0 +1,22 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+set(java_sources
+ src/org/qtproject/qt/android/nfc/QtNfc.java
+ src/org/qtproject/qt/android/nfc/QtNfcBroadcastReceiver.java
+)
+
+qt_internal_add_jar(Qt${QtConnectivity_VERSION_MAJOR}AndroidNfc
+ INCLUDE_JARS ${QT_ANDROID_JAR}
+ SOURCES ${java_sources}
+ OUTPUT_DIR "${QT_BUILD_DIR}/jar"
+)
+
+qt_path_join(destination ${INSTALL_DATADIR} "jar")
+
+install_jar(Qt${QtConnectivity_VERSION_MAJOR}AndroidNfc
+ DESTINATION ${destination}
+ COMPONENT Devel
+)
+
+add_dependencies(Nfc Qt${QtConnectivity_VERSION_MAJOR}AndroidNfc)
diff --git a/src/android/nfc/nfc.pro b/src/android/nfc/nfc.pro
deleted file mode 100644
index 66b1d8a4..00000000
--- a/src/android/nfc/nfc.pro
+++ /dev/null
@@ -1,16 +0,0 @@
-TARGET = QtNfc
-
-CONFIG += java
-DESTDIR = $$[QT_INSTALL_PREFIX/get]/jar
-API_VERSION = android-18
-
-PATHPREFIX = $$PWD/src/org/qtproject/qt5/android/nfc
-
-JAVACLASSPATH += $$PWD/src/
-JAVASOURCES += \
- $$PWD/src/org/qtproject/qt5/android/nfc/QtNfc.java \
- $$PWD/src/org/qtproject/qt5/android/nfc/QtNfcBroadcastReceiver.java \
-
-# install
-target.path = $$[QT_INSTALL_PREFIX]/jar
-INSTALLS += target
diff --git a/src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfc.java b/src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfc.java
index 19e645f5..7753f182 100644
--- a/src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfc.java
+++ b/src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfc.java
@@ -1,49 +1,10 @@
-/****************************************************************************
-**
-** Copyright (C) 2016 Centria research and development
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the QtNfc 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$
-**
-****************************************************************************/
-
-package org.qtproject.qt5.android.nfc;
-
-import java.lang.Thread;
+// Copyright (C) 2016 Centria research and development
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.nfc;
+
import java.lang.Runnable;
-import android.os.Parcelable;
-import android.os.Looper;
import android.content.Context;
import android.app.Activity;
import android.app.PendingIntent;
@@ -51,22 +12,20 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NfcAdapter;
import android.content.IntentFilter.MalformedMimeTypeException;
-import android.os.Bundle;
+import android.os.Build;
+import android.os.Parcelable;
import android.util.Log;
-import android.content.BroadcastReceiver;
import android.content.pm.PackageManager;
-public class QtNfc
+class QtNfc
{
- /* static final QtNfc m_nfc = new QtNfc(); */
static private final String TAG = "QtNfc";
- static public NfcAdapter m_adapter = null;
- static public PendingIntent m_pendingIntent = null;
- static public IntentFilter[] m_filters;
- static public Context m_context = null;
- static public Activity m_activity = null;
+ static private NfcAdapter m_adapter = null;
+ static private PendingIntent m_pendingIntent = null;
+ static private Context m_context = null;
+ static private Activity m_activity = null;
- static public void setContext(Context context)
+ static void setContext(Context context)
{
m_context = context;
if (context instanceof Activity) m_activity = (Activity) context;
@@ -78,42 +37,30 @@ public class QtNfc
}
if (m_adapter == null) {
- //Log.e(TAG, "No NFC available");
return;
}
+ // Since Android 12 (API level 31) it's mandatory to specify mutability
+ // of PendingIntent. We need a mutable intent, which was a default
+ // option earlier.
+ int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) ? PendingIntent.FLAG_MUTABLE
+ : 0;
m_pendingIntent = PendingIntent.getActivity(
m_activity,
0,
new Intent(m_activity, m_activity.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
- 0);
-
- //Log.d(TAG, "Pending intent:" + m_pendingIntent);
-
- IntentFilter filter = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
-
- m_filters = new IntentFilter[]{
- filter
- };
-
- try {
- filter.addDataType("*/*");
- } catch(MalformedMimeTypeException e) {
- throw new RuntimeException("Fail", e);
- }
-
- //Log.d(TAG, "Thread:" + Thread.currentThread().getId());
+ flags);
}
- static public boolean start()
+ static boolean startDiscovery()
{
if (m_adapter == null || m_activity == null
|| !m_activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC))
return false;
m_activity.runOnUiThread(new Runnable() {
+ @Override
public void run() {
- //Log.d(TAG, "Enabling NFC");
IntentFilter[] filters = new IntentFilter[3];
filters[0] = new IntentFilter();
filters[0].addAction(NfcAdapter.ACTION_TAG_DISCOVERED);
@@ -124,9 +71,9 @@ public class QtNfc
try {
filters[1].addDataType("*/*");
} catch (MalformedMimeTypeException e) {
- throw new RuntimeException("Check your mime type.");
+ throw new RuntimeException("IntentFilter.addDataType() failed");
}
- // some tags will report as tech, even if they are ndef formated/formatable.
+ // some tags will report as tech, even if they are ndef formatted/formattable.
filters[2] = new IntentFilter();
filters[2].addAction(NfcAdapter.ACTION_TECH_DISCOVERED);
String[][] techList = new String[][]{
@@ -144,15 +91,15 @@ public class QtNfc
return true;
}
- static public boolean stop()
+ static boolean stopDiscovery()
{
if (m_adapter == null || m_activity == null
|| !m_activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC))
return false;
m_activity.runOnUiThread(new Runnable() {
+ @Override
public void run() {
- //Log.d(TAG, "Disabling NFC");
try {
m_adapter.disableForegroundDispatch(m_activity);
} catch(IllegalStateException e) {
@@ -164,22 +111,21 @@ public class QtNfc
return true;
}
- static public boolean isAvailable()
+ static boolean isEnabled()
{
if (m_adapter == null) {
- //Log.e(TAG, "No NFC available (Adapter is null)");
return false;
}
return m_adapter.isEnabled();
}
- static public boolean isSupported()
+ static boolean isSupported()
{
return (m_adapter != null);
}
- static public Intent getStartIntent()
+ static Intent getStartIntent()
{
Log.d(TAG, "getStartIntent");
if (m_activity == null) return null;
@@ -193,4 +139,9 @@ public class QtNfc
return null;
}
}
+
+ static Parcelable getTag(Intent intent)
+ {
+ return intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ }
}
diff --git a/src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfcBroadcastReceiver.java b/src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfcBroadcastReceiver.java
new file mode 100644
index 00000000..cd6b6a43
--- /dev/null
+++ b/src/android/nfc/src/org/qtproject/qt/android/nfc/QtNfcBroadcastReceiver.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 Governikus GmbH & Co. KG
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+package org.qtproject.qt.android.nfc;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+
+class QtNfcBroadcastReceiver extends BroadcastReceiver
+{
+ final private long qtObject;
+ final private Context qtContext;
+
+ QtNfcBroadcastReceiver(long obj, Context context)
+ {
+ qtObject = obj;
+ qtContext = context;
+ IntentFilter filter = new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED);
+ qtContext.registerReceiver(this, filter);
+ }
+
+ void unregisterReceiver()
+ {
+ qtContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent)
+ {
+ final int state = intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_OFF);
+ jniOnReceive(qtObject, state);
+ }
+
+ native void jniOnReceive(long qtObject, int state);
+}
diff --git a/src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfcBroadcastReceiver.java b/src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfcBroadcastReceiver.java
deleted file mode 100644
index ea650ede..00000000
--- a/src/android/nfc/src/org/qtproject/qt5/android/nfc/QtNfcBroadcastReceiver.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2018 Governikus GmbH & Co. KG
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the QtNfc 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$
-**
-****************************************************************************/
-
-package org.qtproject.qt5.android.nfc;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nfc.NfcAdapter;
-
-
-public class QtNfcBroadcastReceiver extends BroadcastReceiver
-{
- private Context qtContext;
-
- public QtNfcBroadcastReceiver(Context context)
- {
- qtContext = context;
- IntentFilter filter = new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED);
- qtContext.registerReceiver(this, filter);
- }
-
- public void unregisterReceiver()
- {
- qtContext.unregisterReceiver(this);
- }
-
- public void onReceive(Context context, Intent intent)
- {
- final int state = intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_OFF);
- jniOnReceive(state);
- }
-
- public native void jniOnReceive(int state);
-}