From d8473c74e16e2e1d99b97894616ca9a0cd0662c9 Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Mon, 13 Dec 2021 17:29:04 +0100 Subject: QLowEnergyController Windows: fix early disconnect crash on Win 11 QLowEnergyController connection helper object was calling CoInitialize() in connectToDevice() and CoUninitialize() in its destructor. This was working fine on Windows 10, but on Windows 11, in case when the connection attempt is interrupted by a disconnect request, it was crashing. The stack trace showed that Windows 11 is trying to do more COM-related calls after CoUninitialize() is called. Specifically, it was calling CoIncrementMTAUsage(), and some others. This patch replaces CoInitialize(), which uses a single-threaded apartment model, to CoInitializeEx(NULL, COINIT_MULTITHREADED) that uses multi-threaded apartment. It also explicitly clears the ComPtr-based class members before calling CoUninitialize(). These two measures help to prevent the crashes. Although I do not completely understand why it helps. Fixes: QTBUG-98582 Change-Id: I6fe5a877dbc2c0518ba97fddb132c2f87fc397c2 Reviewed-by: Oliver Wolff (cherry picked from commit 6b910367d859504a0bb48b82e6a2400f76e35241) Reviewed-by: Juha Vuolle --- src/bluetooth/qlowenergycontroller_winrt_new.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bluetooth/qlowenergycontroller_winrt_new.cpp b/src/bluetooth/qlowenergycontroller_winrt_new.cpp index e90073a4..b2ebab2b 100644 --- a/src/bluetooth/qlowenergycontroller_winrt_new.cpp +++ b/src/bluetooth/qlowenergycontroller_winrt_new.cpp @@ -404,6 +404,8 @@ public: ~QWinRTLowEnergyConnectionHandler() { qCDebug(QT_BT_WINRT) << __FUNCTION__; + mDevice.Reset(); + mGattSession.Reset(); // To close the COM library gracefully, each successful call to // CoInitialize, including those that return S_FALSE, must be balanced // by a corresponding call to CoUninitialize. @@ -436,7 +438,7 @@ private: void QWinRTLowEnergyConnectionHandler::connectToDevice() { qCDebug(QT_BT_WINRT) << __FUNCTION__; - mInitialized = CoInitialize(NULL); + mInitialized = CoInitializeEx(NULL, COINIT_MULTITHREADED); qCDebug(QT_BT_WINRT) << qt_error_string(mInitialized); auto earlyExit = [this]() { return mAbortConnection; }; -- cgit v1.2.1 From 52a03bcb3cad602175062456aca15ae9a16f6006 Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Fri, 3 Dec 2021 17:54:44 +0100 Subject: Windows QBluetoothSocket: remove unneeded variable from SocketWorker The m_initialReadOp member of the SocketWorker class is used only once, to perform initial read. After that m_readOp is used for all other reads. I see no reason why we need a separate variable for the first read, so this patch uses m_readOp instead. This allows to simplify the code by removing some duplication. Change-Id: If4dd57c9f080d90c975cef676c1bf84cedf44178 Reviewed-by: Oliver Wolff (cherry picked from commit 54a856d8f654fb6183c772b4eaacd92703d22062) Reviewed-by: Alex Blasche --- src/bluetooth/qbluetoothsocket_winrt.cpp | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/bluetooth/qbluetoothsocket_winrt.cpp b/src/bluetooth/qbluetoothsocket_winrt.cpp index f542829b..731d5432 100644 --- a/src/bluetooth/qbluetoothsocket_winrt.cpp +++ b/src/bluetooth/qbluetoothsocket_winrt.cpp @@ -141,20 +141,6 @@ public: void close() { m_shuttingDown = true; - if (Q_UNLIKELY(m_initialReadOp)) { - onReadyRead(m_initialReadOp.Get(), Canceled); - ComPtr info; - HRESULT hr = m_initialReadOp.As(&info); - Q_ASSERT_SUCCEEDED(hr); - if (info) { - hr = info->Cancel(); - Q_ASSERT_SUCCEEDED(hr); - hr = info->Close(); - Q_ASSERT_SUCCEEDED(hr); - } - m_initialReadOp.Reset(); - } - if (m_readOp) { onReadyRead(m_readOp.Get(), Canceled); ComPtr info; @@ -195,9 +181,9 @@ public: ComPtr stream; hr = m_socket->get_InputStream(&stream); Q_ASSERT_SUCCEEDED(hr); - hr = stream->ReadAsync(buffer.Get(), READ_BUFFER_SIZE, InputStreamOptions_Partial, m_initialReadOp.GetAddressOf()); + hr = stream->ReadAsync(buffer.Get(), READ_BUFFER_SIZE, InputStreamOptions_Partial, m_readOp.GetAddressOf()); Q_ASSERT_SUCCEEDED(hr); - hr = m_initialReadOp->put_Completed(Callback(this, &SocketWorker::onReadyRead).Get()); + hr = m_readOp->put_Completed(Callback(this, &SocketWorker::onReadyRead).Get()); Q_ASSERT_SUCCEEDED(hr); return S_OK; }); @@ -209,13 +195,10 @@ public: if (m_shuttingDown) return S_OK; - if (asyncInfo == m_initialReadOp.Get()) { - m_initialReadOp.Reset(); - } else if (asyncInfo == m_readOp.Get()) { + if (asyncInfo == m_readOp.Get()) m_readOp.Reset(); - } else { + else Q_ASSERT(false); - } // A read in UnconnectedState will close the socket and return -1 and thus tell the caller, // that the connection was closed. The socket cannot be closed here, as the subsequent read @@ -323,7 +306,6 @@ private: // Protects pendingData/pendingDatagrams which are accessed from native callbacks QMutex m_mutex; - ComPtr> m_initialReadOp; ComPtr> m_readOp; }; -- cgit v1.2.1 From d68134dc320f3f6fc25973aea9e813e4088e4545 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Mon, 13 Dec 2021 23:08:05 +0200 Subject: Add Info.plists to autotests which require bluetooth usage key This is needed to run the test cases on macOS 12. Also enable the tst_qlowenergycontroller test to run even if the sensor tag device is not found; the test contains functions that don't use it (the ones that do use it will skip). Task-number: QTBUG-98817 QTBUG-98816 Change-Id: I41369b7bfcea4de6c47428657881d9bab05a093e Reviewed-by: Qt CI Bot Reviewed-by: Alex Blasche --- .../qbluetoothdevicediscoveryagent.pro | 2 ++ .../qbluetoothservicediscoveryagent.pro | 2 ++ .../qlowenergycharacteristic.pro | 2 ++ .../qlowenergycontroller/qlowenergycontroller.pro | 2 ++ .../tst_qlowenergycontroller.cpp | 3 ++- .../qlowenergydescriptor/qlowenergydescriptor.pro | 2 ++ tests/auto/shared/Info.macos.plist | 24 ++++++++++++++++++++++ 7 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/auto/shared/Info.macos.plist diff --git a/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro b/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro index 900bb5e9..e221ba74 100644 --- a/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro +++ b/tests/auto/qbluetoothdevicediscoveryagent/qbluetoothdevicediscoveryagent.pro @@ -2,5 +2,7 @@ SOURCES += tst_qbluetoothdevicediscoveryagent.cpp TARGET=tst_qbluetoothdevicediscoveryagent CONFIG += testcase +macos: QMAKE_INFO_PLIST = ../shared/Info.macos.plist + QT = core concurrent bluetooth-private testlib osx:QT += widgets diff --git a/tests/auto/qbluetoothservicediscoveryagent/qbluetoothservicediscoveryagent.pro b/tests/auto/qbluetoothservicediscoveryagent/qbluetoothservicediscoveryagent.pro index 7d4eba6f..4fd115bf 100644 --- a/tests/auto/qbluetoothservicediscoveryagent/qbluetoothservicediscoveryagent.pro +++ b/tests/auto/qbluetoothservicediscoveryagent/qbluetoothservicediscoveryagent.pro @@ -2,6 +2,8 @@ SOURCES += tst_qbluetoothservicediscoveryagent.cpp TARGET = tst_qbluetoothservicediscoveryagent CONFIG += testcase +macos: QMAKE_INFO_PLIST = ../shared/Info.macos.plist + QT = core concurrent bluetooth testlib osx:QT += widgets diff --git a/tests/auto/qlowenergycharacteristic/qlowenergycharacteristic.pro b/tests/auto/qlowenergycharacteristic/qlowenergycharacteristic.pro index 24106573..98562724 100644 --- a/tests/auto/qlowenergycharacteristic/qlowenergycharacteristic.pro +++ b/tests/auto/qlowenergycharacteristic/qlowenergycharacteristic.pro @@ -2,6 +2,8 @@ SOURCES += tst_qlowenergycharacteristic.cpp TARGET = tst_qlowenergycharacteristic CONFIG += testcase +macos: QMAKE_INFO_PLIST = ../shared/Info.macos.plist + QT = core bluetooth testlib diff --git a/tests/auto/qlowenergycontroller/qlowenergycontroller.pro b/tests/auto/qlowenergycontroller/qlowenergycontroller.pro index 7a67e8e4..a3d67ec9 100644 --- a/tests/auto/qlowenergycontroller/qlowenergycontroller.pro +++ b/tests/auto/qlowenergycontroller/qlowenergycontroller.pro @@ -8,6 +8,8 @@ CONFIG += testcase SOURCES += tst_qlowenergycontroller.cpp +macos: QMAKE_INFO_PLIST = ../shared/Info.macos.plist + osx|ios { QT += widgets } diff --git a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp index bc74c693..08590a31 100644 --- a/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp +++ b/tests/auto/qlowenergycontroller/tst_qlowenergycontroller.cpp @@ -176,7 +176,8 @@ void tst_QLowEnergyController::initTestCase() } } - QVERIFY2(deviceFound, "Cannot find remote device."); + if (!deviceFound) + qWarning() << "Unable to find the TI sensor tag device, will skip most of the test"; // These are the services exported by the TI SensorTag #ifndef Q_OS_MAC diff --git a/tests/auto/qlowenergydescriptor/qlowenergydescriptor.pro b/tests/auto/qlowenergydescriptor/qlowenergydescriptor.pro index 81ec9566..00ebe5c2 100644 --- a/tests/auto/qlowenergydescriptor/qlowenergydescriptor.pro +++ b/tests/auto/qlowenergydescriptor/qlowenergydescriptor.pro @@ -2,5 +2,7 @@ SOURCES += tst_qlowenergydescriptor.cpp TARGET = tst_qlowenergydescriptor CONFIG += testcase +macos: QMAKE_INFO_PLIST = ../shared/Info.macos.plist + QT = core bluetooth testlib diff --git a/tests/auto/shared/Info.macos.plist b/tests/auto/shared/Info.macos.plist new file mode 100644 index 00000000..e447b77c --- /dev/null +++ b/tests/auto/shared/Info.macos.plist @@ -0,0 +1,24 @@ + + + + + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundlePackageType + APPL + CFBundleSignature + ???? + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSPrincipalClass + NSApplication + NSBluetoothAlwaysUsageDescription + Qt BT test wants to access your Bluetooth adapter + NSSupportsAutomaticGraphicsSwitching + + + -- cgit v1.2.1 From 1806578f2ef67173153f25a4a20dac501d4c9c6e Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Fri, 3 Dec 2021 18:13:39 +0100 Subject: Windows QBluetoothSocket: fix crash at disconnecting The last onReadyRead callback can come after the object is destroyed. Protect against it by wrapping 'this' in a QPointer, and checking the QPointer in the callback. Fixes: QTBUG-98719 Change-Id: I3d5200e29744012815cd168a340bc617f85c6540 Reviewed-by: Alex Blasche Reviewed-by: Oliver Wolff (cherry picked from commit 79a1523ed6262ccd148173ec8732865c0721063a) Reviewed-by: Juha Vuolle --- src/bluetooth/qbluetoothsocket_winrt.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/bluetooth/qbluetoothsocket_winrt.cpp b/src/bluetooth/qbluetoothsocket_winrt.cpp index 731d5432..a1f7212c 100644 --- a/src/bluetooth/qbluetoothsocket_winrt.cpp +++ b/src/bluetooth/qbluetoothsocket_winrt.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include @@ -183,7 +184,14 @@ public: Q_ASSERT_SUCCEEDED(hr); hr = stream->ReadAsync(buffer.Get(), READ_BUFFER_SIZE, InputStreamOptions_Partial, m_readOp.GetAddressOf()); Q_ASSERT_SUCCEEDED(hr); - hr = m_readOp->put_Completed(Callback(this, &SocketWorker::onReadyRead).Get()); + QPointer thisPtr(this); + hr = m_readOp->put_Completed( + Callback([thisPtr](IAsyncBufferOperation *asyncInfo, + AsyncStatus status) { + if (thisPtr) + return thisPtr->onReadyRead(asyncInfo, status); + return S_OK; + }).Get()); Q_ASSERT_SUCCEEDED(hr); return S_OK; }); @@ -284,7 +292,14 @@ public: emit socketErrorOccured(QBluetoothSocket::UnknownSocketError); return S_OK; } - hr = m_readOp->put_Completed(Callback(this, &SocketWorker::onReadyRead).Get()); + QPointer thisPtr(this); + hr = m_readOp->put_Completed( + Callback([thisPtr](IAsyncBufferOperation *asyncInfo, + AsyncStatus status) { + if (thisPtr) + return thisPtr->onReadyRead(asyncInfo, status); + return S_OK; + }).Get()); if (FAILED(hr)) { qErrnoWarning(hr, "onReadyRead(): Failed to set socket read callback."); emit socketErrorOccured(QBluetoothSocket::UnknownSocketError); -- cgit v1.2.1 From fc26d456cf62263cdc6040e97ef79b7b40b67339 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Mon, 29 Nov 2021 15:47:01 +0200 Subject: Android BT LE Client thread protection improvement The Android BT LE Client is a threaded feature; the Java-side callbacks from Android execute in different threads and JNI calls from Qt/C++ execute in the Qt thread. This commit augments the existing protection by synchronizing the accesses to shared variables on the client object. Task-number: QTBUG-98351 Change-Id: Icb39499c42bbdeec5e65ed5368294e1c631d3bfa Reviewed-by: Andreas Buhr (cherry picked from commit a8e6de3863c89db251e6420a70351860c375b3f3) Reviewed-by: Ivan Solovev --- .../qt5/android/bluetooth/QtBluetoothLE.java | 1044 ++++++++++---------- 1 file changed, 540 insertions(+), 504 deletions(-) diff --git a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java index 094cb501..1e9fd81a 100644 --- a/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java +++ b/src/android/bluetooth/src/org/qtproject/qt5/android/bluetooth/QtBluetoothLE.java @@ -128,6 +128,12 @@ public class QtBluetoothLE { 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, @@ -136,50 +142,54 @@ public class QtBluetoothLE { // 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 class BondStateBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (mBluetoothGatt == null) - return; + 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 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); + 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) { - synchronized (readWriteQueue) { - if (pendingJob == null || pendingJob.jobType == IoJobType.Mtu) - return; - } + if (bondState == BluetoothDevice.BOND_BONDING) { + if (pendingJob == null || pendingJob.jobType == IoJobType.Mtu) + return; - timeoutHandler.removeCallbacksAndMessages(null); - handleForTimeout.set(HANDLE_FOR_RESET); - } else if (previousBondState == BluetoothDevice.BOND_BONDING && (bondState == BluetoothDevice.BOND_BONDED || bondState == BluetoothDevice.BOND_NONE)) { - synchronized (readWriteQueue) { - if (pendingJob == null || pendingJob.jobType == IoJobType.Mtu) - 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) + return; - readWriteQueue.addFirst(pendingJob); - pendingJob = null; - } + 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(); - } + 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; @@ -203,11 +213,11 @@ public class QtBluetoothLE { /*************************************************************/ /* 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 */ /*************************************************************/ - /* - Returns true, if request was successfully completed - */ public boolean scanForLeDevice(final boolean isEnabled) { if (isEnabled == mLeScanRunning) return true; @@ -261,343 +271,419 @@ public class QtBluetoothLE { 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; + private synchronized void handleOnConnectionStateChange(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: - if (bondStateBroadcastReceiver != null) { - qtContext.unregisterReceiver(bondStateBroadcastReceiver); - bondStateBroadcastReceiver = null; - } + 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; - } + 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; 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); + } + 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; } - 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 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()); + //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); + } - scheduleMtuExchange(); + 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 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(); + } - 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) { - pendingJob = null; - } + private synchronized void handleOnCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + 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()); - performNextIO(); - return; - } - } + //unlock the queue for next item + pendingJob = null; - 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; - } + performNextIO(); + return; + } - GattEntry entry = entries.get(foundHandle); - final boolean isServiceDiscoveryRun = !entry.valueKnown; - entry.valueKnown = true; + 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; + } - 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 + 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()); + 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); - } + // This must be in sync with QLowEnergyService::CharacteristicReadError + final int characteristicReadError = 5; + leServiceError(qtObject, foundHandle + 1, characteristicReadError); } + } - if (isServiceDiscoveryRun) { + 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); - } + // 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) + { + int handle = handleForCharacteristic(characteristic); + if (handle == -1) { + Log.w(TAG,"onCharacteristicChanged: cannot find handle"); + return; + } + + leCharacteristicChanged(qtObject, handle+1, characteristic.getValue()); + } + + 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) + { + 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 - synchronized (readWriteQueue) { - pendingJob = null; - } + pendingJob = null; performNextIO(); + return; } - public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt, - android.bluetooth.BluetoothGattCharacteristic characteristic, - int status) - { - if (status != BluetoothGatt.GATT_SUCCESS) - Log.w(TAG, "onCharacteristicWrite: error " + status); + 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; + } - int handle = handleForCharacteristic(characteristic); - if (handle == -1) { - Log.w(TAG,"onCharacteristicWrite: cannot find handle"); - return; - } + GattEntry entry = entries.get(foundHandle); + final boolean isServiceDiscoveryRun = !entry.valueKnown; + entry.valueKnown = true; - 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; + 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); } - 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 + } + + 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); } - byte[] value; - synchronized (readWriteQueue) { - value = pendingJob.newValue; - pendingJob = null; + /* 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); + } } - leCharacteristicWritten(qtObject, handle+1, value, 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; - } + //unlock the queue for next item + pendingJob = null; + + performNextIO(); + } - leCharacteristicChanged(qtObject, handle+1, characteristic.getValue()); + 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; } - 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) { - pendingJob = null; - } - performNextIO(); - 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 + } - 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; - } + pendingJob = null; - GattEntry entry = entries.get(foundHandle); - final boolean isServiceDiscoveryRun = !entry.valueKnown; - entry.valueKnown = true; + leDescriptorWritten(qtObject, handle+1, descriptor.getValue(), errorCode); + performNextIO(); + } - 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); - } + private synchronized void handleOnMtuChanged(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 pendingJob -> the timeout has done that already + return; + } - 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); - } + pendingJob = null; - /* Some devices preset ClientCharacteristicConfiguration descriptors - * to enable notifications out of the box. However the additional - * BluetoothGatt.setCharacteristicNotification call prevents - * automatic notifications from coming through. Hence we manually set them - * up here. - */ - if (descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) { - byte[] bytearray = descriptor.getValue(); - final int value = (bytearray != null && bytearray.length > 0) ? bytearray[0] : 0; - // notification or indication bit set? - if ((value & 0x03) > 0) { - Log.d(TAG, "Found descriptor with automatic notifications."); - mBluetoothGatt.setCharacteristicNotification( - descriptor.getCharacteristic(), true); - } - } - } + performNextIO(); + } - //unlock the queue for next item - synchronized (readWriteQueue) { - pendingJob = null; - } + /*************************************************************/ + /* Service Discovery */ + /*************************************************************/ - performNextIO(); + private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { + + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + super.onConnectionStateChange(gatt, status, newState); + handleOnConnectionStateChange(gatt, status, newState); } - public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt, - android.bluetooth.BluetoothGattDescriptor descriptor, - int status) + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + super.onServicesDiscovered(gatt, status); + handleOnServicesDiscovered(gatt, status); + + } + + public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt, + android.bluetooth.BluetoothGattCharacteristic characteristic, + 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; - } + super.onCharacteristicRead(gatt, characteristic, status); + handleOnCharacteristicRead(gatt, characteristic, status); + } - 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 - } + public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt, + android.bluetooth.BluetoothGattCharacteristic characteristic, + int status) + { + super.onCharacteristicWrite(gatt, characteristic, status); + handleOnCharacteristicWrite(gatt, characteristic, status); + } - synchronized (readWriteQueue) { - pendingJob = null; - } + public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt, + android.bluetooth.BluetoothGattCharacteristic characteristic) + { + super.onCharacteristicChanged(gatt, characteristic); + handleOnCharacteristicChanged(gatt, characteristic); + } - leDescriptorWritten(qtObject, handle+1, descriptor.getValue(), errorCode); - performNextIO(); + public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt, + android.bluetooth.BluetoothGattDescriptor descriptor, + int status) + { + super.onDescriptorRead(gatt, descriptor, status); + handleOnDescriptorRead(gatt, descriptor, status); + } + + public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt, + android.bluetooth.BluetoothGattDescriptor descriptor, + int status) + { + super.onDescriptorWrite(gatt, descriptor, status); + handleOnDescriptorWrite(gatt, descriptor, status); } //TODO Requires Android API 21 which is not available on CI yet. // public void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt, @@ -613,33 +699,13 @@ public class QtBluetoothLE { // 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 pendingJob -> the timeout has done that already - return; - } - - synchronized (readWriteQueue) { - pendingJob = null; - } - - performNextIO(); + super.onMtuChanged(gatt, mtu, status); + handleOnMtuChanged(gatt, mtu, status); } }; - - public boolean connect() { + // This function is called from Qt thread + public synchronized boolean connect() { BluetoothDevice mRemoteGattDevice; try { @@ -734,14 +800,16 @@ public class QtBluetoothLE { return mBluetoothGatt != null; } - public void disconnect() { + // This function is called from Qt thread + public synchronized void disconnect() { if (mBluetoothGatt == null) return; mBluetoothGatt.disconnect(); } - public boolean discoverServices() + // This function is called from Qt thread + public synchronized boolean discoverServices() { return mBluetoothGatt != null && mBluetoothGatt.discoverServices(); } @@ -786,7 +854,6 @@ public class QtBluetoothLE { // index into array is equivalent to handle id private final ArrayList entries = new ArrayList(100); //backlog of to be discovered services - // TODO remove private final LinkedList servicesToBeDiscovered = new LinkedList(); @@ -875,6 +942,7 @@ public class QtBluetoothLE { return -1; } + // This function is called from Qt thread (indirectly) private void populateHandles() { // We introduce the notion of artificial handles. While GATT handles @@ -938,21 +1006,18 @@ public class QtBluetoothLE { private void resetData() { - synchronized (this) { - uuidToEntry.clear(); - entries.clear(); - servicesToBeDiscovered.clear(); - } + uuidToEntry.clear(); + entries.clear(); + servicesToBeDiscovered.clear(); // kill all timeout handlers timeoutHandler.removeCallbacksAndMessages(null); handleForTimeout.set(HANDLE_FOR_RESET); - synchronized (readWriteQueue) { - readWriteQueue.clear(); - } + readWriteQueue.clear(); } + // This function is called from Qt thread public synchronized boolean discoverServiceDetails(String serviceUuid) { try { @@ -1009,9 +1074,9 @@ public class QtBluetoothLE { /* Returns the uuids of the services included by the given service. Otherwise returns null. - Directly called from Qt. + This function is called from Qt thread */ - public String includedServices(String serviceUuid) + public synchronized String includedServices(String serviceUuid) { if (mBluetoothGatt == null) return null; @@ -1041,24 +1106,22 @@ public class QtBluetoothLE { return builder.toString(); } - //TODO function not yet used - private void finishCurrentServiceDiscovery(int handleDiscoveredService) + private synchronized 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"); - } + 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 private boolean executeMtuExchange() { if (Build.VERSION.SDK_INT >= 21) { @@ -1076,7 +1139,7 @@ public class QtBluetoothLE { } catch (Exception ex) {} } - Log.w(TAG, "Assuming default MTU value of 23 bytes"); + Log.w(TAG, "Assuming default MTU value of 23 bytes"); mSupportedMtu = DEFAULT_MTU; return true; @@ -1091,9 +1154,7 @@ public class QtBluetoothLE { newJob.jobType = IoJobType.Mtu; newJob.entry = null; - synchronized (readWriteQueue) { - readWriteQueue.add(newJob); - } + readWriteQueue.add(newJob); performNextIO(); } @@ -1104,7 +1165,6 @@ public class QtBluetoothLE { 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) { @@ -1117,47 +1177,42 @@ public class QtBluetoothLE { 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; - } + // 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; + // 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); - } + 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 */ /*************************************************************/ - public boolean writeCharacteristic(int charHandle, byte[] newValue, + public synchronized boolean writeCharacteristic(int charHandle, byte[] newValue, int writeMode) { if (mBluetoothGatt == null) @@ -1190,9 +1245,7 @@ public class QtBluetoothLE { } boolean result; - synchronized (readWriteQueue) { - result = readWriteQueue.add(newJob); - } + result = readWriteQueue.add(newJob); if (!result) { Log.w(TAG, "Cannot add characteristic write request for " + charHandle + " to queue" ); @@ -1205,9 +1258,10 @@ public class QtBluetoothLE { /*************************************************************/ /* Write Descriptors */ + /* This function is called from Qt thread */ /*************************************************************/ - public boolean writeDescriptor(int descHandle, byte[] newValue) + public synchronized boolean writeDescriptor(int descHandle, byte[] newValue) { if (mBluetoothGatt == null) return false; @@ -1227,9 +1281,7 @@ public class QtBluetoothLE { newJob.jobType = IoJobType.Write; boolean result; - synchronized (readWriteQueue) { - result = readWriteQueue.add(newJob); - } + result = readWriteQueue.add(newJob); if (!result) { Log.w(TAG, "Cannot add descriptor write request for " + descHandle + " to queue" ); @@ -1242,9 +1294,10 @@ public class QtBluetoothLE { /*************************************************************/ /* Read Characteristics */ + /* This function is called from Qt thread */ /*************************************************************/ - public boolean readCharacteristic(int charHandle) + public synchronized boolean readCharacteristic(int charHandle) { if (mBluetoothGatt == null) return false; @@ -1262,9 +1315,7 @@ public class QtBluetoothLE { newJob.jobType = IoJobType.Read; boolean result; - synchronized (readWriteQueue) { - result = readWriteQueue.add(newJob); - } + result = readWriteQueue.add(newJob); if (!result) { Log.w(TAG, "Cannot add characteristic read request for " + charHandle + " to queue" ); @@ -1275,7 +1326,8 @@ public class QtBluetoothLE { return true; } - public boolean readDescriptor(int descHandle) + // This function is called from Qt thread + public synchronized boolean readDescriptor(int descHandle) { if (mBluetoothGatt == null) return false; @@ -1293,9 +1345,7 @@ public class QtBluetoothLE { newJob.jobType = IoJobType.Read; boolean result; - synchronized (readWriteQueue) { - result = readWriteQueue.add(newJob); - } + result = readWriteQueue.add(newJob); if (!result) { Log.w(TAG, "Cannot add descriptor read request for " + descHandle + " to queue" ); @@ -1309,12 +1359,10 @@ public class QtBluetoothLE { // 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) + private synchronized void interruptCurrentIO(int handle) { //unlock the queue for next item - synchronized (readWriteQueue) { - pendingJob = null; - } + pendingJob = null; performNextIOThreaded(); @@ -1322,19 +1370,16 @@ public class QtBluetoothLE { return; try { - synchronized (this) { - - GattEntry entry = entries.get(handle); - if (entry == null) - return; - if (entry.valueKnown) - return; - entry.valueKnown = true; + 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); - } + 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()); @@ -1364,7 +1409,7 @@ public class QtBluetoothLE { 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() + private synchronized void performNextIO() { if (mBluetoothGatt == null) return; @@ -1373,62 +1418,58 @@ public class QtBluetoothLE { final ReadWriteJob nextJob; int handle = HANDLE_FOR_RESET; - synchronized (readWriteQueue) { - if (readWriteQueue.isEmpty() || pendingJob != null) - 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; - } - } + if (readWriteQueue.isEmpty() || pendingJob != null) + return; - // 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); + 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 Write: - skip = executeWriteJob(nextJob); + case Descriptor: + handle = handleForDescriptor(nextJob.entry.descriptor); break; - case Mtu: - skip = executeMtuExchange(); + case CharacteristicValue: + handle = nextJob.entry.endHandle; + default: 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); - } + // 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)); - 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); - } + 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 { + pendingJob = nextJob; + 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; @@ -1477,15 +1518,13 @@ public class QtBluetoothLE { } // 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()); - } + 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; @@ -1517,7 +1556,6 @@ public class QtBluetoothLE { } } - // Runs inside the Mutex on readWriteQueue. // Returns true if nextJob should be skipped. private boolean executeWriteJob(ReadWriteJob nextJob) { @@ -1586,7 +1624,6 @@ public class QtBluetoothLE { return false; } - // Runs inside the Mutex on readWriteQueue. // Returns true if nextJob should be skipped. private boolean executeReadJob(ReadWriteJob nextJob) { @@ -1664,8 +1701,8 @@ public class QtBluetoothLE { return modifiedHandle; } - // Directly called from public Qt API - public boolean requestConnectionUpdatePriority(double minimalInterval) + // This function is called from Qt thread + public synchronized boolean requestConnectionUpdatePriority(double minimalInterval) { if (mBluetoothGatt == null) return false; @@ -1706,4 +1743,3 @@ public class QtBluetoothLE { public native void leCharacteristicChanged(long qtObject, int charHandle, byte[] newData); public native void leServiceError(long qtObject, int attributeHandle, int errorCode); } - -- cgit v1.2.1 From 0ee4082e37cb0f23a5f5aa69fa3291d270e5b1f8 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Sun, 19 Dec 2021 16:42:19 +0100 Subject: IOBluetooth (device scan) - reduce the manual timeout Normally, we set it to 15 s. when creating inquiry instance, since Monterey our 15 s. are ignored and we had a timer to stop the scan in 30 s. Now that tests are runing on CI a painfully long timeout is not needed. Make it 17 s. in case IOBluetooth will be able to stop in 15. Change-Id: I8e07c2a3f3c46bed3780f3287e637b5324660305 Reviewed-by: Juha Vuolle (cherry picked from commit 24674633ecebc85f09fe8bd5babeba9abe0ee3e0) Reviewed-by: Timur Pocheptsov --- src/bluetooth/osx/osxbtdeviceinquiry.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bluetooth/osx/osxbtdeviceinquiry.mm b/src/bluetooth/osx/osxbtdeviceinquiry.mm index 5f95d617..654effdc 100644 --- a/src/bluetooth/osx/osxbtdeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtdeviceinquiry.mm @@ -128,7 +128,9 @@ const uint8_t IOBlueoothInquiryLengthS = 15; }); watchDog->setSingleShot(true); - watchDog->setInterval(IOBlueoothInquiryLengthS * 2 * 1000); // Let's make it twice as long. + // Let's use 17 s. so that IOBluetooth, if it respects 'inquiryLength' + // has a chance to stop before we do: + watchDog->setInterval((IOBlueoothInquiryLengthS + 2) * 1000); watchDog->start(); } -- cgit v1.2.1 From 138f6f0a4e18cdcd88c9ce69653f1d3d34b91491 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Sun, 19 Dec 2021 13:10:07 +0100 Subject: tst_QBluetoothServiceInfo::tst_assignment - fix for Monterey To register a service record 'bluetoothd' now (on Monterey) wants us to have ServiceClassIDList. This fixes the test from RfcommProtocol, but not L2CAP (which we consider non-working in such form). Task-number: QTBUG-98955 Change-Id: I4c8c9e4ca38f0ac86b7fb6cb039b430688b4b359 Reviewed-by: Juha Vuolle (cherry picked from commit b4621669c7bff244ad490a21e3faf1ec51ba28a4) Reviewed-by: Timur Pocheptsov --- .../tst_qbluetoothserviceinfo.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/auto/qbluetoothserviceinfo/tst_qbluetoothserviceinfo.cpp b/tests/auto/qbluetoothserviceinfo/tst_qbluetoothserviceinfo.cpp index f89802d2..895529c7 100644 --- a/tests/auto/qbluetoothserviceinfo/tst_qbluetoothserviceinfo.cpp +++ b/tests/auto/qbluetoothserviceinfo/tst_qbluetoothserviceinfo.cpp @@ -38,6 +38,8 @@ #include #include +#include + QT_USE_NAMESPACE Q_DECLARE_METATYPE(QBluetoothUuid::ProtocolUuid) @@ -187,6 +189,11 @@ void tst_QBluetoothServiceInfo::tst_assignment_data() #if defined(QT_ANDROID_BLUETOOTH) || defined(Q_OS_WIN) l2cpSupported = false; #endif + +#if defined(Q_OS_MACOS) + l2cpSupported = QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSBigSur; +#endif + QTest::newRow("assignment_data_l2cp") << QUuid(0x67c8770b, 0x44f1, 0x410a, 0xab, 0x9a, 0xf9, 0xb5, 0x44, 0x6f, 0x13, 0xee) << QBluetoothUuid::L2cap << QBluetoothServiceInfo::L2capProtocol << l2cpSupported; @@ -348,6 +355,19 @@ void tst_QBluetoothServiceInfo::tst_assignment() serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); +#if defined(Q_OS_MACOS) + // bluetoothd on Monterey does not want to register a record if there is no + // ServiceClassIDList provided. + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) { + // Nothing seems to help with L2CAP though: + if (serviceInfoProtocol == QBluetoothServiceInfo::RfcommProtocol) { + QBluetoothServiceInfo::Sequence classIds; + classIds << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort)); + copyInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classIds); + } + } +#endif // Q_OS_MACOS + QVERIFY(copyInfo.registerService()); QVERIFY(copyInfo.isRegistered()); QVERIFY(serviceInfo.isRegistered()); -- cgit v1.2.1 From 3db3ceae50be5680afebec32a9e56ecaa263ee5f Mon Sep 17 00:00:00 2001 From: Tarja Sundqvist Date: Fri, 31 Dec 2021 15:31:51 +0200 Subject: Bump version --- .qmake.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.qmake.conf b/.qmake.conf index 48f4c9ef..213056f5 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -2,4 +2,4 @@ load(qt_build_config) DEFINES += QT_NO_FOREACH QT_NO_JAVA_STYLE_ITERATORS QT_NO_LINKED_LIST -MODULE_VERSION = 5.15.8 +MODULE_VERSION = 5.15.9 -- cgit v1.2.1 From f43bb5860f860331e6b732036ef231e600fe81aa Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Wed, 19 Jan 2022 15:07:25 +0200 Subject: Store the serviceinfo in pingpong example The service info returned by the QBluetoothServer::listen() overload that returns it needs to be stored or otherwise it will be destroyed. The consequence was that the SDP "advertisement" didn't properly include the added pingpong service record on Windows. Task-number: QTBUG-100042 Change-Id: I79cbd0f574e35bbc1c9c3123ce8382f3aad296d4 Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit debf0fd212b232bcd83c8bb8f51ba2bad2f57a1c) Reviewed-by: Qt Cherry-pick Bot --- examples/bluetooth/pingpong/pingpong.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/bluetooth/pingpong/pingpong.cpp b/examples/bluetooth/pingpong/pingpong.cpp index d80df3d6..f21a57ac 100644 --- a/examples/bluetooth/pingpong/pingpong.cpp +++ b/examples/bluetooth/pingpong/pingpong.cpp @@ -249,7 +249,7 @@ void PingPong::startServer() this, &PingPong::serverError); const QBluetoothUuid uuid(serviceUuid); - m_serverInfo->listen(uuid, QStringLiteral("PingPong server")); + m_serviceInfo = m_serverInfo->listen(uuid, QStringLiteral("PingPong server")); //! [Starting the server] setMessage(QStringLiteral("Server started, waiting for the client. You are the left player.")); // m_role is set to 1 if it is a server -- cgit v1.2.1 From b7531ea9ffc57d2647c1c35d1cb222b8b2dfb3c6 Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Thu, 20 Jan 2022 13:30:47 +0100 Subject: Android: fix signal order during service discovery Android implementation uses timers to check for services with a delay. At some point it could lead to a state, when all discovered devices are handled, but the timers for service discoveries were still active. Before the patch, a finished() signal was emitted in such case, and later, when the timer expires, the services got populated and we received a serviceDiscovered() signal. With this patch, we do not emit a finished() signal when the timers are still pending. Instead the signal is emitted from the slot (which was done anyway). Change-Id: I55485ed842ff45f8aedfdca92b3fb1ee0adbae37 Reviewed-by: Alex Blasche Reviewed-by: Juha Vuolle (cherry picked from commit c24a04ba4beb719ed423f69f80850d0bea5903b9) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothservicediscoveryagent_android.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp index 2b77390a..8947dc0c 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp +++ b/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp @@ -274,9 +274,10 @@ void QBluetoothServiceDiscoveryAgentPrivate::_q_processFetchedUuids( Q_Q(QBluetoothServiceDiscoveryAgent); QTimer::singleShot(4000, q, [this]() { this->_q_fetchUuidsTimeout(); - }); + }); // will also call _q_seriveDiscoveryFinished() + } else { + _q_serviceDiscoveryFinished(); } - _q_serviceDiscoveryFinished(); return; } -- cgit v1.2.1 From e2f13481ee7ea479f0b7c20d61afd833cc6861e1 Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Thu, 20 Jan 2022 11:03:15 +0100 Subject: PingPong example: let the parent QObject do the cleanup The m_timer and m_serverInfo instances are created using 'this' as a parent, so we do not need to explicitly delete them in the destructor, but let the parent object deal with them. This also prevents a crash at application close. Change-Id: If91f01e1ab6dc8e839a06773464eeb9858d2fd36 Reviewed-by: Juha Vuolle Reviewed-by: Alex Blasche (cherry picked from commit 4d2df2461f304eb7e1db5030e1c63d17bc2bac85) Reviewed-by: Qt Cherry-pick Bot --- examples/bluetooth/pingpong/pingpong.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/bluetooth/pingpong/pingpong.cpp b/examples/bluetooth/pingpong/pingpong.cpp index f21a57ac..43575344 100644 --- a/examples/bluetooth/pingpong/pingpong.cpp +++ b/examples/bluetooth/pingpong/pingpong.cpp @@ -64,8 +64,7 @@ PingPong::PingPong(): PingPong::~PingPong() { - delete m_timer; - delete m_serverInfo; + m_timer->stop(); delete socket; delete discoveryAgent; } -- cgit v1.2.1 From 228d5bad4b2aabf93c30393de706cad4d1424f68 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Tue, 25 Jan 2022 09:40:53 +0200 Subject: Make Windows bluetooth to scan all found devices for services The service scan is split in two phases. First we discover the available devices and then we scan each discovered device for their services. The problem was that the Windows bluetooth backend scanned only the device that was discovered the last and then stopped. This patch addresses this by making the scanning logic to follow that of the other backends (Bluez, macOS, and Android). The change required also some adjustments to the stop() logic where the Windows backend did not send the canceled() signal. The worker "cancel logic" is nonexistent and does not produce the signal. A related issue was that the stop() function is called both by the baseclass and the windows specialization itself for slightly differing purposes => needed to be split in two: releaseWorker() and stop(). Fixes: QTBUG-99687 Change-Id: Ie9e25cf0261c5259125dd0f4c6305ef1a99051e7 Reviewed-by: Qt CI Bot Reviewed-by: Ivan Solovev (cherry picked from commit 30390a1285c631b766d98fc8dd79d9d9ec6a5eb0) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothservicediscoveryagent_p.h | 2 +- .../qbluetoothservicediscoveryagent_winrt.cpp | 63 +++++++++------------- .../tst_qbluetoothservicediscoveryagent.cpp | 23 ++++++++ 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_p.h b/src/bluetooth/qbluetoothservicediscoveryagent_p.h index 32ef3cb2..d163a80c 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_p.h +++ b/src/bluetooth/qbluetoothservicediscoveryagent_p.h @@ -239,10 +239,10 @@ private: private slots: void processFoundService(quint64 deviceAddress, const QBluetoothServiceInfo &info); void onScanFinished(quint64 deviceAddress); - void onScanCanceled(); void onError(); private: + void releaseWorker(); QPointer worker; #endif diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp index b4594f21..5ca3382a 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp +++ b/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp @@ -94,7 +94,6 @@ public: Q_SIGNALS: void serviceFound(quint64 deviceAddress, const QBluetoothServiceInfo &info); void scanFinished(quint64 deviceAddress); - void scanCanceled(); void errorOccured(); private: @@ -494,7 +493,7 @@ QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() { - stop(); + releaseWorker(); } void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) @@ -508,8 +507,6 @@ void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &addr this, &QBluetoothServiceDiscoveryAgentPrivate::processFoundService, Qt::QueuedConnection); connect(worker, &QWinRTBluetoothServiceDiscoveryWorker::scanFinished, this, &QBluetoothServiceDiscoveryAgentPrivate::onScanFinished, Qt::QueuedConnection); - connect(worker, &QWinRTBluetoothServiceDiscoveryWorker::scanCanceled, - this, &QBluetoothServiceDiscoveryAgentPrivate::onScanCanceled, Qt::QueuedConnection); connect(worker, &QWinRTBluetoothServiceDiscoveryWorker::errorOccured, this, &QBluetoothServiceDiscoveryAgentPrivate::onError, Qt::QueuedConnection); worker->start(); @@ -517,20 +514,9 @@ void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &addr void QBluetoothServiceDiscoveryAgentPrivate::stop() { - if (!worker) - return; - - disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::serviceFound, - this, &QBluetoothServiceDiscoveryAgentPrivate::processFoundService); - disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::scanFinished, - this, &QBluetoothServiceDiscoveryAgentPrivate::onScanFinished); - disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::scanCanceled, - this, &QBluetoothServiceDiscoveryAgentPrivate::onScanCanceled); - disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::errorOccured, - this, &QBluetoothServiceDiscoveryAgentPrivate::onError); - // mWorker will delete itself as soon as it is done with its discovery - worker = nullptr; - setDiscoveryState(Inactive); + releaseWorker(); + Q_Q(QBluetoothServiceDiscoveryAgent); + emit q->canceled(); } void QBluetoothServiceDiscoveryAgentPrivate::processFoundService(quint64 deviceAddress, const QBluetoothServiceInfo &info) @@ -579,26 +565,12 @@ void QBluetoothServiceDiscoveryAgentPrivate::processFoundService(quint64 deviceA void QBluetoothServiceDiscoveryAgentPrivate::onScanFinished(quint64 deviceAddress) { - Q_Q(QBluetoothServiceDiscoveryAgent); - bool deviceFound; - for (const QBluetoothDeviceInfo &deviceInfo : qAsConst(discoveredDevices)) { - if (deviceInfo.address().toUInt64() == deviceAddress) { - deviceFound = true; - discoveredDevices.removeOne(deviceInfo); - if (discoveredDevices.isEmpty()) - setDiscoveryState(Inactive); - break; - } - } - Q_ASSERT(deviceFound); - stop(); - emit q->finished(); -} - -void QBluetoothServiceDiscoveryAgentPrivate::onScanCanceled() -{ - Q_Q(QBluetoothServiceDiscoveryAgent); - emit q->canceled(); + // The scan for a device's services has finished. Disconnect the + // worker and call the baseclass function which starts the scan for + // the next device if there are any unscanned devices left (or finishes + // the scan if none left) + releaseWorker(); + _q_serviceDiscoveryFinished(); } void QBluetoothServiceDiscoveryAgentPrivate::onError() @@ -610,6 +582,21 @@ void QBluetoothServiceDiscoveryAgentPrivate::onError() emit q->error(error); } +void QBluetoothServiceDiscoveryAgentPrivate::releaseWorker() +{ + if (!worker) + return; + + disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::serviceFound, + this, &QBluetoothServiceDiscoveryAgentPrivate::processFoundService); + disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::scanFinished, + this, &QBluetoothServiceDiscoveryAgentPrivate::onScanFinished); + disconnect(worker, &QWinRTBluetoothServiceDiscoveryWorker::errorOccured, + this, &QBluetoothServiceDiscoveryAgentPrivate::onError); + // mWorker will delete itself as soon as it is done with its discovery + worker = nullptr; +} + QT_END_NAMESPACE #include diff --git a/tests/auto/qbluetoothservicediscoveryagent/tst_qbluetoothservicediscoveryagent.cpp b/tests/auto/qbluetoothservicediscoveryagent/tst_qbluetoothservicediscoveryagent.cpp index 94a065bc..52a0c8bd 100644 --- a/tests/auto/qbluetoothservicediscoveryagent/tst_qbluetoothservicediscoveryagent.cpp +++ b/tests/auto/qbluetoothservicediscoveryagent/tst_qbluetoothservicediscoveryagent.cpp @@ -64,6 +64,7 @@ private slots: void tst_invalidBtAddress(); void tst_serviceDiscovery_data(); void tst_serviceDiscovery(); + void tst_serviceDiscoveryStop(); void tst_serviceDiscoveryAdapters(); private: @@ -141,6 +142,28 @@ void tst_QBluetoothServiceDiscoveryAgent::initTestCase() } } +void tst_QBluetoothServiceDiscoveryAgent::tst_serviceDiscoveryStop() +{ + if (!localDeviceAvailable) + QSKIP("This test requires Bluetooth adapter in powered ON state"); + + QBluetoothServiceDiscoveryAgent discoveryAgent; + QSignalSpy finishedSpy(&discoveryAgent, SIGNAL(finished())); + QSignalSpy canceledSpy(&discoveryAgent, SIGNAL(canceled())); + + // Verify we get the correct signals on start-stop + discoveryAgent.start(QBluetoothServiceDiscoveryAgent::FullDiscovery); + QVERIFY(discoveryAgent.isActive()); + discoveryAgent.stop(); + QTRY_COMPARE(canceledSpy.count(), 1); + QVERIFY(!discoveryAgent.isActive()); + // Wait a bit to see that there are no latent signals + QTest::qWait(200); + QCOMPARE(canceledSpy.count(), 1); + QCOMPARE(finishedSpy.count(), 0); +} + + void tst_QBluetoothServiceDiscoveryAgent::tst_invalidBtAddress() { #ifdef Q_OS_OSX -- cgit v1.2.1 From 6a96181d9ac673a7aa66a9077e2f4f8aba90c30f Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Thu, 20 Jan 2022 13:01:13 +0200 Subject: Decrement the pending paired device counter only once per device The m_pendingPairedDevices variable keeps track of if there are more devices which should be discovered. The counter was decrement twice instead of once leading to possibly non-ending discovery or missing a discovered device. This patch also introduces a helper method to decrement-and-check the amount of pending paired devices. Using this method in classicBluetoothInfoFromDeviceIdAsync allows to properly finish device discovery in case of an async operation error for the last device in the list. Task-number: QTBUG-99685 Done-with: Ivan Solovev Change-Id: Iaa36b212e8754940d9d574a60379fa0c32fdad2c Reviewed-by: Oliver Wolff (cherry picked from commit 0c76617b0cbb7ad4903c2573c206e00bfb6add59) Reviewed-by: Juha Vuolle --- .../qbluetoothdevicediscoveryagent_winrt.cpp | 32 ++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp index e515d4e1..8967e424 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp @@ -157,6 +157,8 @@ private: #endif HRESULT onBluetoothLEAdvertisementReceived(IBluetoothLEAdvertisementReceivedEventArgs *args); + void decrementPairedDevicesAndCheckFinished(); + public slots: void finishDiscovery(); @@ -492,10 +494,7 @@ void QWinRTBluetoothDeviceDiscoveryWorker::classicBluetoothInfoFromDeviceIdAsync // on Windows 10 FromIdAsync might ask for device permission. We cannot wait here but have to handle that asynchronously HRESULT hr = m_deviceStatics->FromIdAsync(deviceId, &deviceFromIdOperation); if (FAILED(hr)) { - --m_pendingPairedDevices; - if (!m_pendingPairedDevices - && !(requestedModes & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) - finishDiscovery(); + decrementPairedDevicesAndCheckFinished(); qCWarning(QT_BT_WINRT) << "Could not obtain bluetooth device from id"; return S_OK; } @@ -506,15 +505,13 @@ void QWinRTBluetoothDeviceDiscoveryWorker::classicBluetoothInfoFromDeviceIdAsync if (thisPointer) { if (status == Completed) thisPointer->onPairedClassicBluetoothDeviceFoundAsync(op, status); - --thisPointer->m_pendingPairedDevices; + else + thisPointer->decrementPairedDevicesAndCheckFinished(); } return S_OK; }).Get()); if (FAILED(hr)) { - --m_pendingPairedDevices; - if (!m_pendingPairedDevices - && !(requestedModes & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) - finishDiscovery(); + decrementPairedDevicesAndCheckFinished(); qCWarning(QT_BT_WINRT) << "Could not register device found callback"; return S_OK; } @@ -522,7 +519,7 @@ void QWinRTBluetoothDeviceDiscoveryWorker::classicBluetoothInfoFromDeviceIdAsync }); if (FAILED(hr)) { emit errorOccured(QBluetoothDeviceDiscoveryAgent::UnknownError); - --m_pendingPairedDevices; + decrementPairedDevicesAndCheckFinished(); qCWarning(QT_BT_WINRT) << "Could not obtain bluetooth device from id"; return; } @@ -531,6 +528,10 @@ void QWinRTBluetoothDeviceDiscoveryWorker::classicBluetoothInfoFromDeviceIdAsync // "deviceFound" will be emitted at the end of the deviceFromIdOperation callback void QWinRTBluetoothDeviceDiscoveryWorker::leBluetoothInfoFromDeviceIdAsync(HSTRING deviceId) { + // Note: in this method we do not need to call + // decrementPairedDevicesAndCheckFinished() because we *do* run LE + // scanning, so the condition in the check will always be false. + // It's enough to just decrement m_pendingPairedDevices. HRESULT hr; hr = QEventDispatcherWinRT::runOnXamlThread([deviceId, this]() { ComPtr> deviceFromIdOperation; @@ -548,7 +549,8 @@ void QWinRTBluetoothDeviceDiscoveryWorker::leBluetoothInfoFromDeviceIdAsync(HSTR if (thisPointer) { if (status == Completed) thisPointer->onPairedBluetoothLEDeviceFoundAsync(op, status); - --thisPointer->m_pendingPairedDevices; + else + --thisPointer->m_pendingPairedDevices; } return S_OK; }).Get()); @@ -687,6 +689,14 @@ HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onPairedClassicBluetoothDeviceFoun return S_OK; } +void QWinRTBluetoothDeviceDiscoveryWorker::decrementPairedDevicesAndCheckFinished() +{ + if ((--m_pendingPairedDevices == 0) + && !(requestedModes & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) { + finishDiscovery(); + } +} + HRESULT QWinRTBluetoothDeviceDiscoveryWorker::onPairedBluetoothLEDeviceFoundAsync(IAsyncOperation *op, AsyncStatus status) { --m_pendingPairedDevices; -- cgit v1.2.1 From fb8c85a5fd1c7170831de06af3f875d39d2a527f Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Wed, 26 Jan 2022 13:50:22 +0200 Subject: Document the bluetooth socket data pausing on macOS Monterey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: QTBUG-99410 Change-Id: I600a861821b87978b9bbcd425d30f49ba6b1aeea Reviewed-by: Timur Pocheptsov Reviewed-by: Tor Arne Vestbø (cherry picked from commit 6224fee61f0e8a77031dbd7edbb87bd77c43d710) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothsocket.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/bluetooth/qbluetoothsocket.cpp b/src/bluetooth/qbluetoothsocket.cpp index e614375f..411d6213 100644 --- a/src/bluetooth/qbluetoothsocket.cpp +++ b/src/bluetooth/qbluetoothsocket.cpp @@ -95,6 +95,11 @@ Q_DECLARE_LOGGING_CATEGORY(QT_BT) On iOS, this class cannot be used because the platform does not expose an API which may permit access to QBluetoothSocket related features. + + \note On macOS Monterey (12) the socket data flow is paused when a + modal dialogue is executing, or an event tracking mode is entered (for + example by long-pressing a Window close button). This may change in the + future releases of macOS. */ /*! -- cgit v1.2.1 From 6c59b203d372acfc340a0e8abf378e9b9a9efc8d Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Tue, 1 Feb 2022 09:48:14 +0200 Subject: Windows BT: reverse the latter UUID part to correct order The Windows IDataReader::ReadGuid function, which is used to read long 128 bit UUIDs from the SDP results, gives the last 8 bytes of the UUID in reverse order. This commit reverses the byte order. Without the correct byte order the UUIDs of the services are reported wrong, and for example the uuidFilter does not work reliably. The original bug was that the "pingpong" Windows client never finds a Linux pingpong server. It turned out that it finds it, but interprets the UUID wrong. The same interpretation error can also be seen against an Android server. Running the pingpong client against an Android server however works for the reason below. This issue has gone undetected so far because the serviceID field is read differently using different interface and method, and as a consequence has made the uuid filters to work against some platforms. Fixes: QTBUG-99689 Change-Id: I71ab44264579f9eb46461ed8fdd7a49dbf402531 Reviewed-by: Ivan Solovev Reviewed-by: Oliver Wolff (cherry picked from commit 0dd124498e91308eb7ac16e976049b431770f7ef) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp index 5ca3382a..170673d3 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp +++ b/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp @@ -82,6 +82,13 @@ Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINRT) #define TYPE_STRING 37 #define TYPE_SEQUENCE 53 +// Helper to reverse given uchar array +static void reverseArray(uchar data[], size_t length) +{ + for (size_t i = length; i > length/2; i--) + std::swap(data[length - i], data[i - 1]); +} + class QWinRTBluetoothServiceDiscoveryWorker : public QObject { Q_OBJECT @@ -308,6 +315,8 @@ void QWinRTBluetoothServiceDiscoveryWorker::processServiceSearchResult(quint64 a GUID value; hr = dataReader->ReadGuid(&value); Q_ASSERT_SUCCEEDED(hr); + // The latter 8 bytes are in reverse order + reverseArray(value.Data4, sizeof(value.Data4)/sizeof(value.Data4[0])); const QBluetoothUuid uuid(value); info.setAttribute(key, uuid); qCDebug(QT_BT_WINRT) << "UUID" << uuid << "KEY" << hex << key << "TYPE" << dec << type << "UUID" << hex << uuid; @@ -421,7 +430,8 @@ QBluetoothServiceInfo::Sequence QWinRTBluetoothServiceDiscoveryWorker::readSeque GUID b; hr = dataReader->ReadGuid(&b); Q_ASSERT_SUCCEEDED(hr); - + // The latter 8 bytes are in reverse order + reverseArray(b.Data4, sizeof(b.Data4)/sizeof(b.Data4[0])); const QBluetoothUuid uuid(b); result.append(QVariant::fromValue(uuid)); remainingLength -= sizeof(GUID); -- cgit v1.2.1 From 4fb1b482d88ddc6c5101bf7922e32199ae6bd52a Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Thu, 20 Jan 2022 15:12:42 +0100 Subject: IOBluetooth: fix SDP inquiry broken by Monterey -performSDPQuery:self - does not open a baseband connection anymore to a device, if not connected yet (despite it's documented to do so). Now we have to ensure isConnected == true first. An attempt to connect is not guaranteed to ever complete (with/out errors), so we need a timeout logic in place. -performSDPQuery:self uuids:filter is simply a noop function on Monterey, we have to filter services manually, after SDP query finished. In addition, it was discovered that service class ID list attribute (its id is 0x0001) as reported by Apple may have a non-sequence type, but UUID itself instead. Fixes: QTBUG-99673 Change-Id: I5464faa0a2eb350f738be0531e726f464c6f2caa Reviewed-by: Juha Vuolle (cherry picked from commit f00ea686c841e14a89bd776ac3b86766a4980bb3) --- src/bluetooth/osx/osxbtsdpinquiry.mm | 112 +++++++++++++++++++-- .../qbluetoothservicediscoveryagent_osx.mm | 18 ++++ 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/bluetooth/osx/osxbtsdpinquiry.mm b/src/bluetooth/osx/osxbtsdpinquiry.mm index 1645f959..481a077b 100644 --- a/src/bluetooth/osx/osxbtsdpinquiry.mm +++ b/src/bluetooth/osx/osxbtsdpinquiry.mm @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2021 The Qt Company Ltd. +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtBluetooth module of the Qt Toolkit. @@ -43,8 +43,12 @@ #include "osxbtutility_p.h" #include "btdelegates_p.h" +#include #include #include +#include + +#include QT_BEGIN_NAMESPACE @@ -52,6 +56,8 @@ namespace OSXBluetooth { namespace { +const int basebandConnectTimeoutMS = 20000; + QBluetoothUuid sdp_element_to_uuid(IOBluetoothSDPDataElement *element) { QT_BT_MAC_AUTORELEASEPOOL; @@ -78,11 +84,20 @@ QVector extract_service_class_ID_list(IOBluetoothSDPServiceRecor QT_BT_MAC_AUTORELEASEPOOL; IOBluetoothSDPDataElement *const idList = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceClassIDList]; - if (!idList || [idList getTypeDescriptor] != kBluetoothSDPDataElementTypeDataElementSequence) - return {}; QVector uuids; - NSArray *const arr = [idList getArrayValue]; + if (!idList) + return uuids; + + NSArray *arr = nil; + if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeDataElementSequence) + arr = [idList getArrayValue]; + else if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeUUID) + arr = @[idList]; + + if (!arr) + return uuids; + for (IOBluetoothSDPDataElement *dataElement in arr) { const auto qtUuid = sdp_element_to_uuid(dataElement); if (!qtUuid.isNull()) @@ -200,7 +215,7 @@ QVector extract_services_uuids(IOBluetoothDevice *device) return uuids; } -} +} // namespace OSXBluetooth QT_END_NAMESPACE @@ -213,6 +228,9 @@ using namespace OSXBluetooth; QT_PREPEND_NAMESPACE(DarwinBluetooth::SDPInquiryDelegate) *delegate; IOBluetoothDevice *device; bool isActive; + + // Needed to workaround a broken SDP on Monterey: + std::unique_ptr connectionWatchdog; } - (id)initWithDelegate:(DarwinBluetooth::SDPInquiryDelegate *)aDelegate @@ -242,6 +260,21 @@ using namespace OSXBluetooth; return [self performSDPQueryWithDevice:address filters:emptyFilter]; } +- (void)interruptSDPQuery +{ + // To be only executed on timer. + Q_ASSERT(connectionWatchdog.get()); + // If device was reset, so the timer should be, we can never be here then. + Q_ASSERT(device); + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + delegate->SDPInquiryError(device, kIOReturnTimeout); + [device release]; + device = nil; + isActive = false; +} + - (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address filters:(const QList &)qtFilters { @@ -252,7 +285,8 @@ using namespace OSXBluetooth; // We first try to allocate "filters": ObjCScopedPointer array; - if (qtFilters.size()) { + if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSBigSur + && qtFilters.size()) { // See the comment about filters on Monterey below. array.reset([[NSMutableArray alloc] init]); if (!array) { qCCritical(QT_BT_OSX) << "failed to allocate an uuid filter"; @@ -282,6 +316,40 @@ using namespace OSXBluetooth; device = newDevice.data(); IOReturn result = kIOReturnSuccess; + + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) { + // SDP query on Monterey does not follow its own documented/expected behavior: + // - a simple performSDPQuery was previously ensuring baseband connection + // to be opened, now it does not do so, instead logs a warning and returns + // immediately. + // - a version with UUID filters simply does nothing except it immediately + // returns kIOReturnSuccess. + + if (![device isConnected]) { + result = [device openConnection:self]; + connectionWatchdog.reset(new QTimer); + connectionWatchdog->setSingleShot(true); + QObject::connect(connectionWatchdog.get(), &QTimer::timeout, + connectionWatchdog.get(), + [self]{[self interruptSDPQuery];}); + connectionWatchdog->start(basebandConnectTimeoutMS); + } + + if ([device isConnected]) + result = [device performSDPQuery:self]; + + if (result != kIOReturnSuccess) { + qCCritical(QT_BT_OSX, "failed to start an SDP query"); + connectionWatchdog.reset(); + device = oldDevice.take(); + } else { + newDevice.take(); + isActive = true; + } + + return result; + } // Monterey's code path. + if (qtFilters.size()) result = [device performSDPQuery:self uuids:array]; else @@ -291,21 +359,42 @@ using namespace OSXBluetooth; qCCritical(QT_BT_OSX) << "failed to start an SDP query"; device = oldDevice.take(); } else { - isActive = true; newDevice.take(); + isActive = true; } return result; } -- (void)stopSDPQuery +- (void)connectionComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status { - // There is no API to stop it, - // but there is a 'stop' member-function in Qt and - // after it's called sdpQueryComplete must be somehow ignored. + if (aDevice != device) { + // Connection was previously cancelled, probably, due to the timeout. + return; + } + + connectionWatchdog.reset(); + if (status == kIOReturnSuccess) + status = [aDevice performSDPQuery:self]; + + if (status != kIOReturnSuccess) { + isActive = false; + qCWarning(QT_BT_OSX, "failed to open connection or start an SDP query"); + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->SDPInquiryError(aDevice, status); + } +} + +- (void)stopSDPQuery +{ + // There is no API to stop it SDP on deice, but there is a 'stop' + // member-function in Qt and after it's called sdpQueryComplete + // must be somehow ignored (device != aDevice in a callback). [device release]; device = nil; + isActive = false; + connectionWatchdog.reset(); } - (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status @@ -327,3 +416,4 @@ using namespace OSXBluetooth; } @end + diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm index d1194c49..8469dd59 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm @@ -47,6 +47,7 @@ #include "osx/osxbluetooth_p.h" #include "osx/uistrings_p.h" +#include #include #include #include @@ -158,6 +159,23 @@ void QBluetoothServiceDiscoveryAgentPrivate::SDPInquiryFinished(void *generic) if (!serviceInfo.isValid()) continue; + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur + && uuidFilter.size()) { + const auto &serviceId = serviceInfo.serviceUuid(); + bool match = !serviceId.isNull() && uuidFilter.contains(serviceId); + if (!match) { + const auto &classUuids = serviceInfo.serviceClassUuids(); + for (const auto &uuid : classUuids) { + if (uuidFilter.contains(uuid)) { + match = true; + break; + } + } + if (!match) + continue; + } + } + if (!isDuplicatedService(serviceInfo)) { discoveredServices.append(serviceInfo); emit q_ptr->serviceDiscovered(serviceInfo); -- cgit v1.2.1 From d6524c24b85ac02ef072da715f118aeb4ba68f32 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Fri, 4 Feb 2022 18:11:47 +0200 Subject: Improve macOS classic bluetooth (server) SDP record creation This commit fixes four SDP record issues 1. The nested sequences were created too "shallow", missing one layer of depth 2. The attribute keys are expected to be in hex format but were given as decimals 3. The matching with variant.canConvert results in too broad acceptance, and for example ushort was interpreted as a string 4. With fix for "3" above in place, the UINT16 attributes were put in as UINT32 because of how macOS by default interprets the NSNumbers as part of SDP dictionary Without these fixes Windows client does not find the service, and Linux client misses some of the details. Fixes: QTBUG-100445 Change-Id: I4f063e49bd8832d0e97bdf69acbd1ee7dacf0f87 Reviewed-by: Timur Pocheptsov (cherry picked from commit 119ef898806d8f67f13877a655b7e2c6b6eb168b) --- src/bluetooth/osx/osxbtservicerecord.mm | 50 ++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/bluetooth/osx/osxbtservicerecord.mm b/src/bluetooth/osx/osxbtservicerecord.mm index 45d1dfe4..d7b191cd 100644 --- a/src/bluetooth/osx/osxbtservicerecord.mm +++ b/src/bluetooth/osx/osxbtservicerecord.mm @@ -146,7 +146,7 @@ void add_attribute(const QVariant &var, AttributeId key, Dictionary dict) return; const Number num(variant_to_nsnumber(var)); - [dict setObject:num forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:num forKey:[NSString stringWithFormat:@"%x", int(key)]]; } template<> @@ -160,7 +160,7 @@ void add_attribute(const QVariant &var, AttributeId key, Dictionary dic const QString string(var.value()); if (string.length()) { if (NSString *const nsString = string.toNSString()) - [dict setObject:nsString forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:nsString forKey:[NSString stringWithFormat:@"%x", int(key)]]; } } @@ -173,7 +173,7 @@ void add_attribute(const QVariant &var, AttributeId key, Diction return; SDPUUid ioUUID(iobluetooth_uuid(var.value())); - [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%x", int(key)]]; } template<> @@ -206,6 +206,25 @@ void add_attribute(const QVariant &var, NSMutableArray *list) [list addObject:num]; } +template<> +void add_attribute(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert()) + return; + + const Number num(variant_to_nsnumber(var)); + + NSDictionary* dict = @{ + @"DataElementType" : [NSNumber numberWithInt:1], + @"DataElementSize" : [NSNumber numberWithInt:2], + @"DataElementValue" : num + }; + + [list addObject: dict]; +} + template<> void add_attribute(const QVariant &var, NSMutableArray *list) { @@ -273,7 +292,7 @@ void add_rfcomm_protocol_descriptor_list(uint16 channelID, Dictionary dict) [rfcommList addObject:rfcommDict]; [descriptorList addObject:rfcommList]; - [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%x", kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; } @@ -299,7 +318,7 @@ void add_l2cap_protocol_descriptor_list(uint16 psm, Dictionary dict) [l2capList addObject:l2capDict]; [descriptorList addObject:l2capList]; - [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%x", kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; } @@ -310,10 +329,10 @@ bool add_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) if (var.canConvert()) return false; - if (var.canConvert()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute(var, list); - } else if (var.canConvert()) { + } else if (var.userType() == qMetaTypeId()) { add_attribute(var, list); } else { // Here we need 'key' to understand the type. @@ -325,6 +344,9 @@ bool add_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) case QSInfo::ServiceInfoTimeToLive: add_attribute(var, list); break; + case QSInfo::BluetoothProfileDescriptorList: + add_attribute(var, list); + break; case QSInfo::ServiceAvailability: add_attribute(var, list); break; @@ -348,10 +370,10 @@ bool add_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeId key, Di if (var.canConvert()) return false; - if (var.canConvert()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute(var, key, dict); - } else if (var.canConvert()) { + } else if (var.userType() == qMetaTypeId()) { add_attribute(serviceInfo.attribute(key), key, dict); } else { // We can have different integer types actually, so I have to check @@ -385,14 +407,15 @@ bool add_sequence_attribute(const QVariant &var, AttributeId key, NSMutableArray if (var.isNull() || !var.canConvert()) return false; + NSMutableArray *const nested = [NSMutableArray array]; + [list addObject:nested]; + const Sequence sequence(var.value()); for (const QVariant &var : sequence) { if (var.canConvert()) { - NSMutableArray *const nested = [NSMutableArray array]; add_sequence_attribute(var, key, nested); - [list addObject:nested]; } else { - add_attribute(var, key, list); + add_attribute(var, key, nested); } } @@ -415,8 +438,7 @@ bool add_sequence_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeI if (!add_sequence_attribute(element, key, list)) add_attribute(element, key, list); } - [dict setObject:list forKey:[NSString stringWithFormat:@"%d", int(key)]]; - + [dict setObject:list forKey:[NSString stringWithFormat:@"%x", int(key)]]; return true; } -- cgit v1.2.1 From 26bb2825f6447703b1829bf266205cdf4b045762 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Mon, 7 Feb 2022 16:58:02 +0200 Subject: Keep the bluetooth service name if the remote has provided it For example the "Bt Chat Server" example application service would always show up as a "Custom Service". Change-Id: If73423d90d5681ee0c3655ef7ee3ad6cd7638755 Reviewed-by: Ivan Solovev (cherry picked from commit e27c6600eca2c9b704c73a0b6d28780eb236cefb) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp index 1207ac92..4493b7a4 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp +++ b/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp @@ -320,7 +320,10 @@ void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceD for (const QBluetoothUuid &id : serviceClassUuids) { if (id.minimumSize() == 16) { serviceInfo.setServiceUuid(id); - serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service")); + if (serviceInfo.serviceName().isEmpty()) { + serviceInfo.setServiceName( + QBluetoothServiceDiscoveryAgent::tr("Custom Service")); + } QBluetoothServiceInfo::Sequence modSeq = serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds).value(); modSeq.removeOne(QVariant::fromValue(id)); -- cgit v1.2.1 From ca0c8f9f4a7ae9efab431eea38bca9f812af8751 Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Thu, 3 Feb 2022 14:21:11 +0100 Subject: Fix missing MetaType registration This patch amends bf69c830419a7cda87d6aa71c962804a946011ea. After fixing the thread affinity issue, Qt is using QueuedConnection for the charListObtained signal. As a result, the signal's arguments need to be registered for the metatype system. This patch provides such registration. This is a 5.15-only change, as Qt 6 is does not require to register the types explicitly. Fixes: QTBUG-99767 Change-Id: I50e6314669bd6e779b85373f417c92b57f23a7e9 Reviewed-by: Fabian Kosmale Reviewed-by: Oliver Wolff --- src/bluetooth/qlowenergycontroller_winrt_new.cpp | 13 +++++++++++++ src/bluetooth/qlowenergyserviceprivate_p.h | 1 + 2 files changed, 14 insertions(+) diff --git a/src/bluetooth/qlowenergycontroller_winrt_new.cpp b/src/bluetooth/qlowenergycontroller_winrt_new.cpp index b2ebab2b..19324300 100644 --- a/src/bluetooth/qlowenergycontroller_winrt_new.cpp +++ b/src/bluetooth/qlowenergycontroller_winrt_new.cpp @@ -678,10 +678,23 @@ void QWinRTLowEnergyConnectionHandler::emitConnectedAndQuitThread() QThread::currentThread()->quit(); } +static void registerServiceHandlerMetaTypes() +{ + static bool registered = false; + if (!registered) { + qRegisterMetaType>( + "QHash"); + qRegisterMetaType>("QVector"); + qRegisterMetaType("QLowEnergyHandle"); + registered = true; + } +} + QLowEnergyControllerPrivateWinRTNew::QLowEnergyControllerPrivateWinRTNew() : QLowEnergyControllerPrivate() { registerQLowEnergyControllerMetaType(); + registerServiceHandlerMetaTypes(); connect(this, &QLowEnergyControllerPrivateWinRTNew::characteristicChanged, this, &QLowEnergyControllerPrivateWinRTNew::handleCharacteristicChanged, Qt::QueuedConnection); diff --git a/src/bluetooth/qlowenergyserviceprivate_p.h b/src/bluetooth/qlowenergyserviceprivate_p.h index 5727d97c..b1d95597 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -146,5 +146,6 @@ typedef QHash DescriptorDa QT_END_NAMESPACE Q_DECLARE_METATYPE(QSharedPointer) +Q_DECLARE_METATYPE(QLowEnergyServicePrivate::CharData) #endif // QLOWENERGYSERVICEPRIVATE_P_H -- cgit v1.2.1 From 475734595b4105e0972a58ef45d32d68b1d4125f Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Mon, 20 Dec 2021 12:20:12 +0100 Subject: IOBluetooth: tweak a couple of classes 1. Connection monitor: instead of asserting on 'monitor != nullptr' - issue a warning and bail-out early. Despite of us unregistering from connection notifications and trying to release the monitor, IOBluetooth seems to keep references and sometimes it's possible to trigger this assert in our autotests. 2. IOBluetoothDeviceInquiry - stop reporting errors completely, they are not mapped to any error in QtBluetooth and give nothing but failing auto-tests. Change-Id: I8fc2e23d3685b912759df91c65b02537f39d1b5f Reviewed-by: Juha Vuolle (cherry picked from commit ba38f64e22c98ba6b0d3bdccbd00ea5a643404f8) Reviewed-by: Timur Pocheptsov --- src/bluetooth/osx/osxbtconnectionmonitor.mm | 9 +++++++++ src/bluetooth/osx/osxbtdeviceinquiry.mm | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/bluetooth/osx/osxbtconnectionmonitor.mm b/src/bluetooth/osx/osxbtconnectionmonitor.mm index bc955c75..811fa765 100644 --- a/src/bluetooth/osx/osxbtconnectionmonitor.mm +++ b/src/bluetooth/osx/osxbtconnectionmonitor.mm @@ -96,6 +96,15 @@ using namespace QT_NAMESPACE; if (!device) return; + if (!monitor) { + // Rather surprising: monitor == nullptr means we stopped monitoring. + // So apparently this thingie is still alive and keeps receiving + // notifications. + qCWarning(QT_BT_OSX, + "Connection notification received in a monitor that was cancelled"); + return; + } + QT_BT_MAC_AUTORELEASEPOOL; // All Obj-C objects are autoreleased. diff --git a/src/bluetooth/osx/osxbtdeviceinquiry.mm b/src/bluetooth/osx/osxbtdeviceinquiry.mm index 654effdc..6048a5c4 100644 --- a/src/bluetooth/osx/osxbtdeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtdeviceinquiry.mm @@ -160,11 +160,11 @@ const uint8_t IOBlueoothInquiryLengthS = 15; if (error != kIOReturnSuccess && !aborted) { // QtBluetooth has not too many error codes, 'UnknownError' is not really - // useful, report the actual error code here: + // useful, log the actual error code here: qCWarning(QT_BT_OSX) << "IOKit error code: " << error; - m_delegate->error(error); // Let watchDog to stop it, calling -stop at timeout, otherwise, - // it looks like inquiry continues and keeps reporting new devices found. + // it looks like inquiry continues even after this error and + // keeps reporting new devices found. } else { // Either a normal completion or from a timer slot. watchDog.reset(); -- cgit v1.2.1 From 2eede8a018ef1a7b1ce18656abbaa50ee95426c5 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Tue, 8 Feb 2022 13:22:29 +0200 Subject: Avoid multiple client sockets in pingpong bluetooth example This commit guards against creating multiple client sockets if multiple services are found. In addition the service scanning is stopped when a service has been found and we start connecting a bluetooth socket. Fixes: QTBUG-100289 Change-Id: Iac2e7eaca17a186ac2d2f62e338be16911f08032 Reviewed-by: Ivan Solovev Reviewed-by: Oliver Wolff (cherry picked from commit 788c4980e67386d2407c4c7bc7182430fc9054c3) Reviewed-by: Qt Cherry-pick Bot --- examples/bluetooth/pingpong/pingpong.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/bluetooth/pingpong/pingpong.cpp b/examples/bluetooth/pingpong/pingpong.cpp index 43575344..aaa625f3 100644 --- a/examples/bluetooth/pingpong/pingpong.cpp +++ b/examples/bluetooth/pingpong/pingpong.cpp @@ -343,7 +343,12 @@ void PingPong::done() void PingPong::addService(const QBluetoothServiceInfo &service) { - setMessage("Service found. Setting parameters..."); + if (m_serviceFound) + return; + m_serviceFound = true; + + setMessage("Service found. Stopping discovery and creating connection..."); + discoveryAgent->stop(); //! [Connecting the socket] socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); socket->connectToService(service); @@ -352,7 +357,6 @@ void PingPong::addService(const QBluetoothServiceInfo &service) connect(socket, &QBluetoothSocket::connected, this, &PingPong::serverConnected); connect(socket, &QBluetoothSocket::disconnected, this, &PingPong::serverDisconnected); //! [Connecting the socket] - m_serviceFound = true; } void PingPong::serviceScanError(QBluetoothServiceDiscoveryAgent::Error error) -- cgit v1.2.1 From ceac17d0e99984ad1fef6f26d215f9ee5cbc65bd Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Fri, 11 Feb 2022 15:25:40 +0200 Subject: Improve bluetooth service discovery on macOS Monterey The original problem is that on macOS Monterey the service discovery, and subsequent usage of the services, is unreliable. This commit improves the situation a bit by checking the connectivity status of the device instead of relying on IOBluetooth callbacks which quite often don't arrive. Also there was a crash due to stray callbacks from IOBluetooth. These seem to cease when we now close the connection we have established for SDP inquiry purposes. The crash happened especially if the remote device got paired during the service scan. In addition this commit introduces more event logging which helps when debugging these issues. Fixes: QTBUG-100303 Change-Id: Id063401390b141cff412cbb47d20d441cc394d3f Reviewed-by: Timur Pocheptsov (cherry picked from commit 2e09bf6c57c8cb0c1069b562ab2451ec8b68c27a) --- src/bluetooth/osx/osxbtdeviceinquiry.mm | 9 ++- src/bluetooth/osx/osxbtsdpinquiry.mm | 68 ++++++++++++++++++---- .../qbluetoothservicediscoveryagent_osx.mm | 7 ++- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/bluetooth/osx/osxbtdeviceinquiry.mm b/src/bluetooth/osx/osxbtdeviceinquiry.mm index 6048a5c4..719e2060 100644 --- a/src/bluetooth/osx/osxbtdeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtdeviceinquiry.mm @@ -112,11 +112,14 @@ const uint8_t IOBlueoothInquiryLengthS = 15; m_active = true; [m_inquiry clearFoundDevices]; + + qCDebug(QT_BT_OSX) << "Starting device inquiry with" + << IOBlueoothInquiryLengthS << "second timeout limit."; const IOReturn result = [m_inquiry start]; if (result != kIOReturnSuccess) { // QtBluetooth will probably convert an error into UnknownError, // losing the actual information. - qCWarning(QT_BT_OSX) << "failed with IOKit error code:" << result; + qCWarning(QT_BT_OSX) << "device inquiry start failed with IOKit error code:" << result; m_active = false; } else { // Docs say it's 10 s. by default, we set it to 15 s. (see -initWithDelegate:), @@ -124,6 +127,7 @@ const uint8_t IOBlueoothInquiryLengthS = 15; watchDog.reset(new QTimer); watchDog->connect(watchDog.get(), &QTimer::timeout, watchDog.get(), [self]{ qCWarning(QT_BT_OSX, "Manually interrupting IOBluetoothDeviceInquiry"); + qCDebug(QT_BT_OSX) << "Found devices:" << [m_inquiry foundDevices]; [self stop]; }); @@ -150,6 +154,8 @@ const uint8_t IOBlueoothInquiryLengthS = 15; - (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry *)sender error:(IOReturn)error aborted:(BOOL)aborted { + qCDebug(QT_BT_OSX) << "deviceInquiryComplete, error:" << error + << "user-stopped:" << aborted; if (!m_active) return; @@ -176,6 +182,7 @@ const uint8_t IOBlueoothInquiryLengthS = 15; - (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry *)sender device:(IOBluetoothDevice *)device { + qCDebug(QT_BT_OSX) << "deviceInquiryDeviceFound:" << [device nameOrAddress]; if (sender != m_inquiry) // Can never happen in the current version. return; diff --git a/src/bluetooth/osx/osxbtsdpinquiry.mm b/src/bluetooth/osx/osxbtsdpinquiry.mm index 481a077b..13d22745 100644 --- a/src/bluetooth/osx/osxbtsdpinquiry.mm +++ b/src/bluetooth/osx/osxbtsdpinquiry.mm @@ -268,6 +268,13 @@ using namespace OSXBluetooth; Q_ASSERT(device); Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + qCDebug(QT_BT_OSX) << "couldn't connect to device" << [device nameOrAddress] + << ", ending SDP inquiry."; + + // Stop the watchdog and close the connection as otherwise there could be + // later "connectionComplete" callbacks + connectionWatchdog->stop(); + [device closeConnection]; delegate->SDPInquiryError(device, kIOReturnTimeout); [device release]; @@ -280,6 +287,7 @@ using namespace OSXBluetooth; { Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress"); Q_ASSERT_X(!address.isNull(), Q_FUNC_INFO, "invalid target device address"); + qCDebug(QT_BT_OSX) << "Starting and SDP inquiry for address:" << address; QT_BT_MAC_AUTORELEASEPOOL; @@ -315,6 +323,8 @@ using namespace OSXBluetooth; ObjCScopedPointer oldDevice(device); device = newDevice.data(); + qCDebug(QT_BT_OSX) << "Device" << [device nameOrAddress] << "connected:" + << bool([device isConnected]) << "paired:" << bool([device isPaired]); IOReturn result = kIOReturnSuccess; if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) { @@ -325,22 +335,44 @@ using namespace OSXBluetooth; // - a version with UUID filters simply does nothing except it immediately // returns kIOReturnSuccess. + // If the device was not yet connected, connect it first if (![device isConnected]) { + qCDebug(QT_BT_OSX) << "Device" << [device nameOrAddress] + << "is not connected, connecting it first"; result = [device openConnection:self]; - connectionWatchdog.reset(new QTimer); - connectionWatchdog->setSingleShot(true); - QObject::connect(connectionWatchdog.get(), &QTimer::timeout, - connectionWatchdog.get(), - [self]{[self interruptSDPQuery];}); - connectionWatchdog->start(basebandConnectTimeoutMS); - } + // The connection may succeed immediately. But if it didn't, start a connection timer + // which has two guardian roles: + // 1. Guard against connect attempt taking too long time + // 2. Sometimes on Monterey the callback indicating "connection completion" is + // not received even though the connection has in fact succeeded + if (![device isConnected]) { + qCDebug(QT_BT_OSX) << "Starting connection monitor for device" + << [device nameOrAddress] << "with timeout limit of" + << basebandConnectTimeoutMS/1000 << "seconds."; + connectionWatchdog.reset(new QTimer); + connectionWatchdog->setSingleShot(false); + QObject::connect(connectionWatchdog.get(), &QTimer::timeout, + connectionWatchdog.get(), + [self] () { + qCDebug(QT_BT_OSX) << "Connection monitor timeout for device:" + << [device nameOrAddress] + << ", connected:" << bool([device isConnected]); + // Device can sometimes get properly connected without IOBluetooth + // calling the connectionComplete callback, so we check the status here + if ([device isConnected]) + [self connectionComplete:device status:kIOReturnSuccess]; + else + [self interruptSDPQuery]; + }); + connectionWatchdog->start(basebandConnectTimeoutMS); + } + } if ([device isConnected]) result = [device performSDPQuery:self]; if (result != kIOReturnSuccess) { qCCritical(QT_BT_OSX, "failed to start an SDP query"); - connectionWatchdog.reset(); device = oldDevice.take(); } else { newDevice.take(); @@ -368,12 +400,17 @@ using namespace OSXBluetooth; - (void)connectionComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status { + qCDebug(QT_BT_OSX) << "connectionComplete for device" << [aDevice nameOrAddress] + << "with status:" << status; if (aDevice != device) { // Connection was previously cancelled, probably, due to the timeout. return; } - connectionWatchdog.reset(); + // The connectionComplete may be invoked by either the IOBluetooth callback or our + // connection watchdog. In either case stop the watchdog if it exists + if (connectionWatchdog) + connectionWatchdog->stop(); if (status == kIOReturnSuccess) status = [aDevice performSDPQuery:self]; @@ -388,7 +425,7 @@ using namespace OSXBluetooth; - (void)stopSDPQuery { - // There is no API to stop it SDP on deice, but there is a 'stop' + // There is no API to stop it SDP on device, but there is a 'stop' // member-function in Qt and after it's called sdpQueryComplete // must be somehow ignored (device != aDevice in a callback). [device release]; @@ -399,6 +436,8 @@ using namespace OSXBluetooth; - (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status { + qCDebug(QT_BT_OSX) << "sdpQueryComplete for device:" << [aDevice nameOrAddress] + << "with status:" << status; // Can happen - there is no legal way to cancel an SDP query, // after the 'reset' device can never be // the same as the cancelled one. @@ -409,6 +448,15 @@ using namespace OSXBluetooth; isActive = false; + // If we used the manual connection establishment, close the + // connection here. Otherwise the IOBluetooth may call stray + // connectionComplete or sdpQueryCompletes + if (connectionWatchdog) { + qCDebug(QT_BT_OSX) << "Closing the connection established for SDP inquiry."; + connectionWatchdog.reset(); + [device closeConnection]; + } + if (status != kIOReturnSuccess) delegate->SDPInquiryError(aDevice, status); else diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm index 8469dd59..49085724 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm @@ -149,15 +149,20 @@ void QBluetoothServiceDiscoveryAgentPrivate::SDPInquiryFinished(void *generic) QT_BT_MAC_AUTORELEASEPOOL; NSArray *const records = device.services; + qCDebug(QT_BT_OSX) << "SDP finished for device" << [device nameOrAddress] + << ", services found:" << [records count]; for (IOBluetoothSDPServiceRecord *record in records) { QBluetoothServiceInfo serviceInfo; Q_ASSERT_X(discoveredDevices.size() >= 1, Q_FUNC_INFO, "invalid number of devices"); + qCDebug(QT_BT_OSX) << "Processing service" << [record getServiceName]; serviceInfo.setDevice(discoveredDevices.at(0)); OSXBluetooth::extract_service_record(record, serviceInfo); - if (!serviceInfo.isValid()) + if (!serviceInfo.isValid()) { + qCDebug(QT_BT_OSX) << "Discarding invalid service"; continue; + } if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur && uuidFilter.size()) { -- cgit v1.2.1 From d1a95f4a5a6d27858c28d63d2b0ec6d643f1fcaf Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Wed, 16 Feb 2022 13:35:33 +0200 Subject: Fix Linux bluetooth service discovery crash with multiple services The application code may call stop() for the service discovery agent when it has detected the service-of-interest. The crash occurs because the stop() will clear the list of discovered devices, but the service discovery result handling loop may still be in the middle of processing the services. If the loop accesses the by-now cleared device list on its next iteration, it will cause a list access violation assert. Fixes: QTBUG-100894 Change-Id: Ica300cd8461543b533800ca06551b21d9b256613 Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit 2de33f78ec374ce6963b9c1715e4942c8cf70bb0) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp index 4493b7a4..6d884cc8 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp +++ b/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp @@ -337,8 +337,10 @@ void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceD qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString() << serviceInfo.serviceName() << serviceInfo.serviceUuid() << ">>>" << serviceInfo.serviceClassUuids(); - - emit q->serviceDiscovered(serviceInfo); + // Use queued connection to allow us finish the service looping; the application + // might call stop() when it has detected the service-of-interest. + QMetaObject::invokeMethod(q, "serviceDiscovered", Qt::QueuedConnection, + Q_ARG(QBluetoothServiceInfo, serviceInfo)); } } } -- cgit v1.2.1 From 6198f70308ee1da16274aada6781824a691566e1 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Fri, 18 Feb 2022 13:28:13 +0200 Subject: Use the service's socket protocol info on Bluez bluetooth socket The QBluetoothSocket::connectToService(QBluetoothServiceInfo) should extract the socket protocol info (rfcomm/l2cap) from the provided service info. Fixes: QTBUG-101018 Change-Id: Ibbf4daef28a2661e4699759d6f834779d27ac750 Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit 29b51e28a961a09fb25d668da5a5e3c9d89390b9) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothsocket_bluezdbus.cpp | 4 ++++ tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bluetooth/qbluetoothsocket_bluezdbus.cpp b/src/bluetooth/qbluetoothsocket_bluezdbus.cpp index 3ce7e8b6..1b644a6d 100644 --- a/src/bluetooth/qbluetoothsocket_bluezdbus.cpp +++ b/src/bluetooth/qbluetoothsocket_bluezdbus.cpp @@ -284,6 +284,10 @@ void QBluetoothSocketPrivateBluezDBus::connectToService( return; } + if (service.socketProtocol() != QBluetoothServiceInfo::Protocol::UnknownProtocol) + socketType = service.socketProtocol(); + qCDebug(QT_BT_BLUEZ) << "Socket protocol used:" << socketType; + connectToService(service.device().address(), targetService, openMode); } diff --git a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp index 05bc1a0f..a7b5ef1f 100644 --- a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp +++ b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp @@ -142,7 +142,7 @@ void tst_QBluetoothSocket::initTestCase() qDebug() << "Starting discovery"; sda->setUuidFilter(QBluetoothUuid(QString(TEST_SERVICE_UUID))); - sda->start(QBluetoothServiceDiscoveryAgent::MinimalDiscovery); + sda->start(QBluetoothServiceDiscoveryAgent::FullDiscovery); for (int connectTime = MaxConnectTime; !done_discovery && connectTime > 0; connectTime -= 1000) QTest::qWait(1000); -- cgit v1.2.1 From 9b165d89c14acb5d67383a956a779fba7708b716 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Fri, 18 Feb 2022 16:48:40 +0200 Subject: Accommodate different bluez socket types in autotest [secFlags] The bluezdbus and bluez socket are initialized to different security flag values. Adjust the test according to which backend is in use. Change-Id: I05f7c10743de8b02e701bcee891fa3be12d4975f Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit bc83b98295c36c28367a64722188164cde1f796b) Reviewed-by: Qt CI Bot --- tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp index a7b5ef1f..9e0dfc54 100644 --- a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp +++ b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp @@ -35,6 +35,9 @@ #include #include #include +#if QT_CONFIG(bluez) +#include +#endif QT_USE_NAMESPACE @@ -506,7 +509,12 @@ void tst_QBluetoothSocket::tst_preferredSecurityFlags() #if defined(QT_ANDROID_BLUETOOTH) | defined(QT_OSX_BLUETOOTH) QCOMPARE(socket.preferredSecurityFlags(), QBluetooth::Secure); #elif QT_CONFIG(bluez) - QCOMPARE(socket.preferredSecurityFlags(), QBluetooth::Authorization); + // The bluezdbus socket uses "NoSecurity" by default, whereas the non-dbus bluez + // socket uses "Authorization" by default + if (bluetoothdVersion() >= QVersionNumber(5, 42)) + QCOMPARE(socket.preferredSecurityFlags(), QBluetooth::Security::NoSecurity); + else + QCOMPARE(socket.preferredSecurityFlags(), QBluetooth::Security::Authorization); #else QCOMPARE(socket.preferredSecurityFlags(), QBluetooth::NoSecurity); #endif -- cgit v1.2.1 From 19ab9a4b8839f0fa201295a065a89e721687f28e Mon Sep 17 00:00:00 2001 From: Andreas Buhr Date: Mon, 28 Feb 2022 18:32:14 +0100 Subject: Repair tst_QBluetoothDeviceDiscoveryAgent on Android tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryMethods was broken on Android because devices with unknown core configuration are found, while the test tested that every device found has either LowEnergyCoreConfiguration or BaseRateCoreConfiguration. Change-Id: I4dc9f50dba989f7827842169dbd06c847a019505 Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit ab7b1ec29f6cc1c8490fa71116a82e18789c4ee3) Reviewed-by: Qt Cherry-pick Bot --- .../tst_qbluetoothdevicediscoveryagent.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp b/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp index cbce5042..f403f4ea 100644 --- a/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp +++ b/tests/auto/qbluetoothdevicediscoveryagent/tst_qbluetoothdevicediscoveryagent.cpp @@ -597,7 +597,9 @@ void tst_QBluetoothDeviceDiscoveryAgent::tst_discoveryMethods() const QBluetoothDeviceInfo info = qvariant_cast(discoveredSpy.takeFirst().at(0)); QVERIFY(info.isValid()); - QVERIFY(info.coreConfigurations() & expectedConfiguration); + // on Android we do find devices with unknown configuration + if (info.coreConfigurations() != QBluetoothDeviceInfo::UnknownCoreConfiguration) + QVERIFY(info.coreConfigurations() & expectedConfiguration); } } -- cgit v1.2.1 From 70ed6f2df679fde389a8953841b5807ba21ba52c Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Sat, 26 Feb 2022 16:07:52 +0200 Subject: Handle LE enhanced connection complete in Bluez peripheral Newer bluetooth devices may trigger (only) a HCI_LE_Enhanced_Connection_Complete instead of HCI_LE_Connection_Complete. This commit adds handling of this so we get a proper bluetooth handle. Task-number: QTBUG-101309 Change-Id: Ibb5cf8ca063df9345a0ef0bcb12ae0dd780bab78 Reviewed-by: Alex Blasche Reviewed-by: Ivan Solovev (cherry picked from commit 12afa962ebb5a6eac4b598bdd1b1804c9a3ca089) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/bluez/hcimanager.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bluetooth/bluez/hcimanager.cpp b/src/bluetooth/bluez/hcimanager.cpp index 44178a2b..82dc119f 100644 --- a/src/bluetooth/bluez/hcimanager.cpp +++ b/src/bluetooth/bluez/hcimanager.cpp @@ -563,9 +563,11 @@ void HciManager::handleHciAclPacket(const quint8 *data, int size) void HciManager::handleLeMetaEvent(const quint8 *data) { - // Spec v4.2, Vol 2, part E, 7.7.65ff + // Spec v5.3, Vol 4, part E, 7.7.65.* switch (*data) { - case 0x1: { + case 0x1: // HCI_LE_Connection_Complete + case 0xA: // HCI_LE_Enhanced_Connection_Complete + { const quint16 handle = bt_get_le16(data + 2); emit connectionComplete(handle); break; -- cgit v1.2.1 From 2b3b5c899ebdb0fc11273af7370804f5f03ad9e8 Mon Sep 17 00:00:00 2001 From: Juha Vuolle Date: Mon, 14 Mar 2022 13:03:27 +0200 Subject: Add QIODevice::canReadLine call to QBluetoothSocket At least on Linux the QIODevice parent class reads and buffers a large chunk of data when readLine() is called. This call effectively empties the private linear data buffer, and subsequent canReadLine() calls to the private buffer returns false. This is problematic if the data contained several lines of data; the QBluetoothSocket::canReadLine() returns false but readLine() returns valid lines of data. This commit adds a parent class call so that it's buffers are also taken into account as per QIODevice documentation. Fixes: QTBUG-101690 Change-Id: I8130aff217e9e6c5525101901ed55721430b6dd0 Reviewed-by: Ivan Solovev Reviewed-by: Alex Blasche (cherry picked from commit 393fd47ab64ce80fb0e852027591840aef8e135d) Reviewed-by: Qt Cherry-pick Bot --- src/bluetooth/qbluetoothsocket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bluetooth/qbluetoothsocket.cpp b/src/bluetooth/qbluetoothsocket.cpp index 411d6213..864b9cab 100644 --- a/src/bluetooth/qbluetoothsocket.cpp +++ b/src/bluetooth/qbluetoothsocket.cpp @@ -598,7 +598,7 @@ void QBluetoothSocket::setSocketState(QBluetoothSocket::SocketState state) bool QBluetoothSocket::canReadLine() const { Q_D(const QBluetoothSocketBase); - return d->canReadLine(); + return d->canReadLine() || QIODevice::canReadLine(); } /*! -- cgit v1.2.1