diff options
Diffstat (limited to 'src/android')
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); -} |