diff options
30 files changed, 993 insertions, 628 deletions
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 diff --git a/examples/bluetooth/pingpong/pingpong.cpp b/examples/bluetooth/pingpong/pingpong.cpp index d80df3d6..aaa625f3 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; } @@ -249,7 +248,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 @@ -344,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); @@ -353,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) 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 96243af8..5fc3ef41 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<BluetoothGattService> services = mBluetoothGatt.getServices(); - for (BluetoothGattService service: services) { - builder.append(service.getUuid().toString()).append(" "); //space is separator - } - break; - default: - Log.w(TAG, "Unhandled error code on onServicesDiscovered: " + status); - errorCode = status; break; //TODO deal with all errors - } - leServicesDiscovered(qtObject, errorCode, builder.toString()); + //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<BluetoothGattService> services = mBluetoothGatt.getServices(); + for (BluetoothGattService service: services) { + builder.append(service.getUuid().toString()).append(" "); //space is separator + } + break; + default: + Log.w(TAG, "Unhandled error code on onServicesDiscovered: " + status); + errorCode = status; break; //TODO deal with all errors } + leServicesDiscovered(qtObject, errorCode, builder.toString()); + if (status == BluetoothGatt.GATT_SUCCESS) + scheduleMtuExchange(); + } - 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<GattEntry> entries = new ArrayList<GattEntry>(100); //backlog of to be discovered services - // TODO remove private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>(); @@ -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); } - diff --git a/src/bluetooth/bluez/hcimanager.cpp b/src/bluetooth/bluez/hcimanager.cpp index e2635fae..a8b8e3b9 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; diff --git a/src/bluetooth/osx/osxbtconnectionmonitor.mm b/src/bluetooth/osx/osxbtconnectionmonitor.mm index f41bbed5..159bceb5 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 2fd0d2db..4757331c 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,11 +127,14 @@ 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]; }); 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(); } @@ -148,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; @@ -158,11 +166,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(); @@ -174,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 a2b02b1a..e60b9787 100644 --- a/src/bluetooth/osx/osxbtsdpinquiry.mm +++ b/src/bluetooth/osx/osxbtsdpinquiry.mm @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 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 <QtCore/qoperatingsystemversion.h> #include <QtCore/qvariant.h> #include <QtCore/qstring.h> +#include <QtCore/qtimer.h> + +#include <memory> 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<QBluetoothUuid> extract_service_class_ID_list(IOBluetoothSDPServiceRecor QT_BT_MAC_AUTORELEASEPOOL; IOBluetoothSDPDataElement *const idList = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceClassIDList]; - if (!idList || [idList getTypeDescriptor] != kBluetoothSDPDataElementTypeDataElementSequence) - return {}; QVector<QBluetoothUuid> 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<QBluetoothUuid> 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<QTimer> connectionWatchdog; } - (id)initWithDelegate:(DarwinBluetooth::SDPInquiryDelegate *)aDelegate @@ -242,17 +260,41 @@ 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)"); + 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]; + device = nil; + isActive = false; +} + - (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address filters:(const QList<QBluetoothUuid> &)qtFilters { 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; // We first try to allocate "filters": ObjCScopedPointer<NSMutableArray> 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"; @@ -281,7 +323,65 @@ using namespace OSXBluetooth; ObjCScopedPointer<IOBluetoothDevice> 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) { + // 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 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]; + // 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"); + 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,25 +391,53 @@ 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. + 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; + } + + // 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]; + + 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 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]; device = nil; + isActive = false; + connectionWatchdog.reset(); } - (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. @@ -320,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 @@ -327,3 +464,4 @@ using namespace OSXBluetooth; } @end + diff --git a/src/bluetooth/osx/osxbtservicerecord.mm b/src/bluetooth/osx/osxbtservicerecord.mm index 23d55d13..2d868f63 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<ValueType>(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<QString>(const QVariant &var, AttributeId key, Dictionary dic const QString string(var.value<QString>()); 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<QBluetoothUuid>(const QVariant &var, AttributeId key, Diction return; SDPUUid ioUUID(iobluetooth_uuid(var.value<QBluetoothUuid>())); - [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%d", int(key)]]; + [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%x", int(key)]]; } template<> @@ -207,6 +207,25 @@ void add_attribute(const QVariant &var, NSMutableArray *list) } template<> +void add_attribute<unsigned short>(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<unsigned short>()) + return; + + const Number num(variant_to_nsnumber<unsigned short>(var)); + + NSDictionary* dict = @{ + @"DataElementType" : [NSNumber numberWithInt:1], + @"DataElementSize" : [NSNumber numberWithInt:2], + @"DataElementValue" : num + }; + + [list addObject: dict]; +} + +template<> void add_attribute<QString>(const QVariant &var, NSMutableArray *list) { Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); @@ -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<Sequence>()) return false; - if (var.canConvert<QString>()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute<QString>(var, list); - } else if (var.canConvert<QBluetoothUuid>()) { + } else if (var.userType() == qMetaTypeId<QBluetoothUuid>()) { add_attribute<QBluetoothUuid>(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<unsigned>(var, list); break; + case QSInfo::BluetoothProfileDescriptorList: + add_attribute<unsigned short>(var, list); + break; case QSInfo::ServiceAvailability: add_attribute<unsigned char>(var, list); break; @@ -348,10 +370,10 @@ bool add_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeId key, Di if (var.canConvert<Sequence>()) return false; - if (var.canConvert<QString>()) { + if (var.type() == QVariant::String) { //ServiceName, ServiceDescription, ServiceProvider. add_attribute<QString>(var, key, dict); - } else if (var.canConvert<QBluetoothUuid>()) { + } else if (var.userType() == qMetaTypeId<QBluetoothUuid>()) { add_attribute<QBluetoothUuid>(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<Sequence>()) return false; + NSMutableArray *const nested = [NSMutableArray array]; + [list addObject:nested]; + const Sequence sequence(var.value<Sequence>()); for (const QVariant &var : sequence) { if (var.canConvert<Sequence>()) { - 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; } diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothdevicediscoveryagent_winrt.cpp index 66834827..82a2ebbc 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<IAsyncOperation<BluetoothLEDevice *>> 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<BluetoothLEDevice *> *op, AsyncStatus status) { --m_pendingPairedDevices; diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_android.cpp index 3ab0d580..96da5ddc 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; } diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_bluez.cpp index 6a93143b..4a18cfc7 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<QBluetoothServiceInfo::Sequence>(); modSeq.removeOne(QVariant::fromValue(id)); @@ -334,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)); } } } diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm index d8decae1..96034ca7 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 <QtCore/qoperatingsystemversion.h> #include <QtCore/qloggingcategory.h> #include <QtCore/qscopedpointer.h> #include <QtCore/qstring.h> @@ -148,15 +149,37 @@ 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()) { + 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); diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_p.h b/src/bluetooth/qbluetoothservicediscoveryagent_p.h index 41410b70..0527cdde 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<QWinRTBluetoothServiceDiscoveryWorker> worker; #endif diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp b/src/bluetooth/qbluetoothservicediscoveryagent_winrt.cpp index f1476758..e8d64efc 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 @@ -94,7 +101,6 @@ public: Q_SIGNALS: void serviceFound(quint64 deviceAddress, const QBluetoothServiceInfo &info); void scanFinished(quint64 deviceAddress); - void scanCanceled(); void errorOccured(); private: @@ -309,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; @@ -422,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); @@ -494,7 +503,7 @@ QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() { - stop(); + releaseWorker(); } void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) @@ -508,8 +517,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 +524,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 +575,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 +592,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 <qbluetoothservicediscoveryagent_winrt.moc> diff --git a/src/bluetooth/qbluetoothsocket.cpp b/src/bluetooth/qbluetoothsocket.cpp index e4d85447..60a64375 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. */ /*! @@ -593,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(); } /*! diff --git a/src/bluetooth/qbluetoothsocket_bluezdbus.cpp b/src/bluetooth/qbluetoothsocket_bluezdbus.cpp index 084aa958..d6aa17a7 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/src/bluetooth/qbluetoothsocket_winrt.cpp b/src/bluetooth/qbluetoothsocket_winrt.cpp index d562f6f7..e7a42f01 100644 --- a/src/bluetooth/qbluetoothsocket_winrt.cpp +++ b/src/bluetooth/qbluetoothsocket_winrt.cpp @@ -50,6 +50,7 @@ #include <QtBluetooth/qbluetoothdeviceinfo.h> #include <QtBluetooth/qbluetoothserviceinfo.h> #include <QtCore/qloggingcategory.h> +#include <QtCore/QPointer> #include <robuffer.h> #include <windows.devices.bluetooth.h> @@ -141,20 +142,6 @@ public: void close() { m_shuttingDown = true; - if (Q_UNLIKELY(m_initialReadOp)) { - onReadyRead(m_initialReadOp.Get(), Canceled); - ComPtr<IAsyncInfo> 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<IAsyncInfo> info; @@ -195,9 +182,16 @@ public: ComPtr<IInputStream> 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<SocketReadCompletedHandler>(this, &SocketWorker::onReadyRead).Get()); + QPointer<SocketWorker> thisPtr(this); + hr = m_readOp->put_Completed( + Callback<SocketReadCompletedHandler>([thisPtr](IAsyncBufferOperation *asyncInfo, + AsyncStatus status) { + if (thisPtr) + return thisPtr->onReadyRead(asyncInfo, status); + return S_OK; + }).Get()); Q_ASSERT_SUCCEEDED(hr); return S_OK; }); @@ -209,13 +203,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 @@ -301,7 +292,14 @@ public: emit socketErrorOccured(QBluetoothSocket::UnknownSocketError); return S_OK; } - hr = m_readOp->put_Completed(Callback<SocketReadCompletedHandler>(this, &SocketWorker::onReadyRead).Get()); + QPointer<SocketWorker> thisPtr(this); + hr = m_readOp->put_Completed( + Callback<SocketReadCompletedHandler>([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); @@ -323,7 +321,6 @@ private: // Protects pendingData/pendingDatagrams which are accessed from native callbacks QMutex m_mutex; - ComPtr<IAsyncOperationWithProgress<IBuffer *, UINT32>> m_initialReadOp; ComPtr<IAsyncOperationWithProgress<IBuffer *, UINT32>> m_readOp; }; diff --git a/src/bluetooth/qlowenergycontroller_winrt_new.cpp b/src/bluetooth/qlowenergycontroller_winrt_new.cpp index 42f4380e..6fd0ed2a 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; }; @@ -676,10 +678,23 @@ void QWinRTLowEnergyConnectionHandler::emitConnectedAndQuitThread() QThread::currentThread()->quit(); } +static void registerServiceHandlerMetaTypes() +{ + static bool registered = false; + if (!registered) { + qRegisterMetaType<QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData>>( + "QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData>"); + qRegisterMetaType<QVector<QBluetoothUuid>>("QVector<QBluetoothUuid>"); + qRegisterMetaType<QLowEnergyHandle>("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 226af145..1816a23f 100644 --- a/src/bluetooth/qlowenergyserviceprivate_p.h +++ b/src/bluetooth/qlowenergyserviceprivate_p.h @@ -146,5 +146,6 @@ typedef QHash<QLowEnergyHandle, QLowEnergyServicePrivate::DescData> DescriptorDa QT_END_NAMESPACE Q_DECLARE_METATYPE(QSharedPointer<QLowEnergyServicePrivate>) +Q_DECLARE_METATYPE(QLowEnergyServicePrivate::CharData) #endif // QLOWENERGYSERVICEPRIVATE_P_H 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/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<QBluetoothDeviceInfo>(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); } } 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/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 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 <qbluetoothuuid.h> #include <QtBluetooth/QBluetoothServer> +#include <QtCore/qoperatingsystemversion.h> + 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()); diff --git a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp index 05bc1a0f..9e0dfc54 100644 --- a/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp +++ b/tests/auto/qbluetoothsocket/tst_qbluetoothsocket.cpp @@ -35,6 +35,9 @@ #include <qbluetoothserviceinfo.h> #include <qbluetoothservicediscoveryagent.h> #include <qbluetoothlocaldevice.h> +#if QT_CONFIG(bluez) +#include <QtBluetooth/private/bluez5_helper_p.h> +#endif QT_USE_NAMESPACE @@ -142,7 +145,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); @@ -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 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>LSMinimumSystemVersion</key> + <string>${MACOSX_DEPLOYMENT_TARGET}</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>NSBluetoothAlwaysUsageDescription</key> + <string>Qt BT test wants to access your Bluetooth adapter</string> + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> +</dict> +</plist> |