Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
QtBluetoothLE.java
Go to the documentation of this file.
1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4package org.qtproject.qt.android.bluetooth;
5
6import android.bluetooth.BluetoothAdapter;
7import android.bluetooth.BluetoothDevice;
8import android.bluetooth.BluetoothGatt;
9import android.bluetooth.BluetoothGattCallback;
10import android.bluetooth.BluetoothGattCharacteristic;
11import android.bluetooth.BluetoothGattDescriptor;
12import android.bluetooth.BluetoothGattService;
13import android.bluetooth.BluetoothProfile;
14import android.bluetooth.BluetoothManager;
15import android.bluetooth.le.BluetoothLeScanner;
16import android.bluetooth.le.ScanCallback;
17import android.bluetooth.le.ScanFilter;
18import android.bluetooth.le.ScanResult;
19import android.bluetooth.le.ScanSettings;
20import android.bluetooth.BluetoothStatusCodes;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.os.Build;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.os.Looper;
29import android.util.Log;
30import java.lang.reflect.Constructor;
31import java.lang.reflect.Method;
32import java.util.concurrent.atomic.AtomicInteger;
33
34import java.util.ArrayList;
35import java.util.Hashtable;
36import java.util.LinkedList;
37import java.util.List;
38import java.util.NoSuchElementException;
39import java.util.UUID;
40
41
42class QtBluetoothLE {
43 private static final String TAG = "QtBluetoothGatt";
44 private BluetoothAdapter mBluetoothAdapter = null;
45 private boolean mLeScanRunning = false;
46
47 private BluetoothGatt mBluetoothGatt = null;
48 private HandlerThread mHandlerThread = null;
49 private Handler mHandler = null;
50 private Constructor mCharacteristicConstructor = null;
51 private String mRemoteGattAddress;
52 private final UUID clientCharacteristicUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
53 private final int MAX_MTU = 512;
54 private final int DEFAULT_MTU = 23;
55 private int mSupportedMtu = -1;
56
57 /*
58 * The atomic synchronizes the timeoutRunnable thread and the response thread for the pending
59 * I/O job. Whichever thread comes first will pass the atomic gate. The other thread is
60 * cut short.
61 */
62 // handle values above zero are for regular handle specific read/write requests
63 // handle values below zero are reserved for handle-independent requests
64 private int HANDLE_FOR_RESET = -1;
65 private int HANDLE_FOR_MTU_EXCHANGE = -2;
66 private int HANDLE_FOR_RSSI_READ = -3;
67 private AtomicInteger handleForTimeout = new AtomicInteger(HANDLE_FOR_RESET); // implies not running by default
68
69 private final int RUNNABLE_TIMEOUT = 3000; // 3 seconds
70 private final Handler timeoutHandler = new Handler(Looper.getMainLooper());
71
72 private BluetoothLeScanner mBluetoothLeScanner = null;
73
74 private class TimeoutRunnable implements Runnable {
75 TimeoutRunnable(int handle) { pendingJobHandle = handle; }
76 @Override
77 public void run() {
78 boolean timeoutStillValid = handleForTimeout.compareAndSet(pendingJobHandle, HANDLE_FOR_RESET);
79 if (timeoutStillValid) {
80 Log.w(TAG, "****** Timeout for request on handle " + (pendingJobHandle & 0xffff));
81 Log.w(TAG, "****** Looks like the peripheral does NOT act in " +
82 "accordance to Bluetooth 4.x spec.");
83 Log.w(TAG, "****** Please check server implementation. Continuing under " +
84 "reservation.");
85
86 if (pendingJobHandle > HANDLE_FOR_RESET)
87 interruptCurrentIO(pendingJobHandle & 0xffff);
88 else if (pendingJobHandle < HANDLE_FOR_RESET)
89 interruptCurrentIO(pendingJobHandle);
90 }
91 }
92
93 // contains handle (0xffff) and top 2 byte contain the job type (0xffff0000)
94 private int pendingJobHandle = -1;
95 };
96
97 // The handleOn* functions in this class are callback handlers which are synchronized
98 // to "this" client object. This protects the member variables which could be
99 // concurrently accessed from Qt (JNI) thread and different Java threads *)
100 // *) The newer Android API (starting Android 8.1) synchronizes callbacks to one
101 // Java thread, but this is not true for the earlier API which we still support.
102 //
103 // In case bond state has been changed due to access to a restricted handle,
104 // Android never completes the operation which triggered the devices to bind
105 // and thus never fires on(Characteristic|Descriptor)(Read|Write) callback,
106 // causing TimeoutRunnable to interrupt pending job,
107 // albeit the read/write job hasn't been actually executed by the peripheral;
108 // re-add the currently pending job to the queue's head and re-run it.
109 // If, by some reason, bonding process has been interrupted, either
110 // re-add the currently pending job to the queue's head and re-run it.
111 private synchronized void handleOnReceive(Context context, Intent intent)
112 {
113 if (mBluetoothGatt == null)
114 return;
115
116 final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
117 if (device == null || !device.getAddress().equals(mBluetoothGatt.getDevice().getAddress()))
118 return;
119
120 final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
121 final int previousBondState =
122 intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);
123
124 if (bondState == BluetoothDevice.BOND_BONDING) {
125 if (pendingJob == null
126 || pendingJob.jobType == IoJobType.Mtu || pendingJob.jobType == IoJobType.Rssi) {
127 return;
128 }
129
130 timeoutHandler.removeCallbacksAndMessages(null);
131 handleForTimeout.set(HANDLE_FOR_RESET);
132 } else if (previousBondState == BluetoothDevice.BOND_BONDING &&
133 (bondState == BluetoothDevice.BOND_BONDED || bondState == BluetoothDevice.BOND_NONE)) {
134 if (pendingJob == null
135 || pendingJob.jobType == IoJobType.Mtu || pendingJob.jobType == IoJobType.Rssi) {
136 return;
137 }
138
139 readWriteQueue.addFirst(pendingJob);
140 pendingJob = null;
141
142 performNextIO();
143 } else if (previousBondState == BluetoothDevice.BOND_BONDED
144 && bondState == BluetoothDevice.BOND_NONE) {
145 // peripheral or central removed the bond information;
146 // if it was peripheral, the connection attempt would fail with PIN_OR_KEY_MISSING,
147 // which is handled by Android by broadcasting ACTION_BOND_STATE_CHANGED
148 // with new state BOND_NONE, without actually deleting the bond information :facepalm:
149 // if we get there, it is safer to delete it now, by invoking the undocumented API call
150 try {
151 device.getClass().getMethod("removeBond").invoke(device);
152 } catch (Exception ex) {
153 ex.printStackTrace();
154 }
155 }
156 }
157
158 private class BondStateBroadcastReceiver extends BroadcastReceiver {
159 @Override
160 public void onReceive(Context context, Intent intent) {
161 handleOnReceive(context, intent);
162 }
163 };
164 private BroadcastReceiver bondStateBroadcastReceiver = null;
165
166 /* Pointer to the Qt object that "owns" the Java object */
167 @SuppressWarnings({"CanBeFinal", "WeakerAccess"})
168 long qtObject = 0;
169 @SuppressWarnings("WeakerAccess")
170 Context qtContext = null;
171
172 @SuppressWarnings("WeakerAccess")
173 QtBluetoothLE(Context context) {
174 qtContext = context;
175
176 BluetoothManager manager =
177 (BluetoothManager)qtContext.getSystemService(Context.BLUETOOTH_SERVICE);
178 if (manager == null)
179 return;
180
181 mBluetoothAdapter = manager.getAdapter();
182 if (mBluetoothAdapter == null)
183 return;
184
185 mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
186 }
187
188 QtBluetoothLE(final String remoteAddress, Context context) {
189 this(context);
190 mRemoteGattAddress = remoteAddress;
191 }
192
193 /*************************************************************/
194 /* Device scan */
195 /* Returns true, if request was successfully completed */
196 /* This function is called from Qt thread, but only accesses */
197 /* variables that are not accessed from Java threads */
198 /*************************************************************/
199
200 boolean scanForLeDevice(final boolean isEnabled) {
201 if (isEnabled == mLeScanRunning)
202 return true;
203
204 if (mBluetoothLeScanner == null) {
205 Log.w(TAG, "Cannot start LE scan, no bluetooth scanner");
206 return false;
207 }
208
209 if (isEnabled) {
210 Log.d(TAG, "Attempting to start BTLE scan");
211 ScanSettings.Builder settingsBuilder = new ScanSettings.Builder();
212 settingsBuilder = settingsBuilder.setScanMode(ScanSettings.SCAN_MODE_BALANCED);
213 ScanSettings settings = settingsBuilder.build();
214
215 List<ScanFilter> filterList = new ArrayList<ScanFilter>();
216
217 mBluetoothLeScanner.startScan(filterList, settings, leScanCallback);
218 mLeScanRunning = true;
219 } else {
220 Log.d(TAG, "Attempting to stop BTLE scan");
221 try {
222 mBluetoothLeScanner.stopScan(leScanCallback);
223 } catch (IllegalStateException isex) {
224 // when trying to stop a scan while bluetooth is offline
225 // java.lang.IllegalStateException: BT Adapter is not turned ON
226 Log.d(TAG, "Stopping LE scan not possible: " + isex.getMessage());
227 }
228 mLeScanRunning = false;
229 }
230
231 return (mLeScanRunning == isEnabled);
232 }
233
234 private final ScanCallback leScanCallback = new ScanCallback() {
235 @Override
236 public void onScanResult(int callbackType, ScanResult result) {
237 super.onScanResult(callbackType, result);
238 leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
239 }
240
241 @Override
242 public void onBatchScanResults(List<ScanResult> results) {
243 super.onBatchScanResults(results);
244 for (ScanResult result : results)
245 leScanResult(qtObject, result.getDevice(), result.getRssi(), result.getScanRecord().getBytes());
246
247 }
248
249 @Override
250 public void onScanFailed(int errorCode) {
251 super.onScanFailed(errorCode);
252 Log.d(TAG, "BTLE device scan failed with " + errorCode);
253 }
254 };
255
256 native void leScanResult(long qtObject, BluetoothDevice device, int rssi, byte[] scanRecord);
257
258 private synchronized void handleOnConnectionStateChange(BluetoothGatt gatt,
259 int status, int newState) {
260
261 Log.d(TAG, "Connection state changes to: " + newState + ", status: " + status
262 + ", qtObject: " + (qtObject != 0));
263 if (qtObject == 0)
264 return;
265
266 int qLowEnergyController_State = 0;
267 //This must be in sync with QLowEnergyController::ControllerState
268 switch (newState) {
269 case BluetoothProfile.STATE_DISCONNECTED:
270 if (bondStateBroadcastReceiver != null) {
271 qtContext.unregisterReceiver(bondStateBroadcastReceiver);
272 bondStateBroadcastReceiver = null;
273 }
274
275 qLowEnergyController_State = 0;
276 // we disconnected -> get rid of data from previous run
277 resetData();
278 // reset mBluetoothGatt, reusing same object is not very reliable
279 // sometimes it reconnects and sometimes it does not.
280 if (mBluetoothGatt != null) {
281 mBluetoothGatt.close();
282 if (mHandler != null) {
283 mHandler.getLooper().quitSafely();
284 mHandler = null;
285 }
286 }
287 mBluetoothGatt = null;
288 break;
289 case BluetoothProfile.STATE_CONNECTED:
290 if (bondStateBroadcastReceiver == null) {
291 bondStateBroadcastReceiver = new BondStateBroadcastReceiver();
292 qtContext.registerReceiver(bondStateBroadcastReceiver,
293 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
294 }
295 qLowEnergyController_State = 2;
296 }
297
298 //This must be in sync with QLowEnergyController::Error
299 int errorCode;
300 switch (status) {
301 case BluetoothGatt.GATT_SUCCESS:
302 errorCode = 0; //QLowEnergyController::NoError
303 break;
304 case BluetoothGatt.GATT_FAILURE: // Android's equivalent of "do not know what error"
305 errorCode = 1; //QLowEnergyController::UnknownError
306 break;
307 case 8: // BLE_HCI_CONNECTION_TIMEOUT
308 Log.w(TAG, "Connection Error: Try to delay connect() call after previous activity");
309 errorCode = 5; //QLowEnergyController::ConnectionError
310 break;
311 case 19: // BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION
312 case 20: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_LOW_RESOURCES
313 case 21: // BLE_HCI_REMOTE_DEV_TERMINATION_DUE_TO_POWER_OFF
314 Log.w(TAG, "The remote host closed the connection");
315 errorCode = 7; //QLowEnergyController::RemoteHostClosedError
316 break;
317 case 22: // BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION
318 // Internally, Android maps PIN_OR_KEY_MISSING to GATT_CONN_TERMINATE_LOCAL_HOST
319 errorCode = 8; //QLowEnergyController::AuthorizationError
320 break;
321 default:
322 Log.w(TAG, "Unhandled error code on connectionStateChanged: "
323 + status + " " + newState);
324 errorCode = status;
325 break; //TODO deal with all errors
326 }
327 leConnectionStateChange(qtObject, errorCode, qLowEnergyController_State);
328 }
329
330 private synchronized void handleOnServicesDiscovered(BluetoothGatt gatt, int status) {
331 //This must be in sync with QLowEnergyController::Error
332 int errorCode;
333 StringBuilder builder = new StringBuilder();
334 switch (status) {
335 case BluetoothGatt.GATT_SUCCESS:
336 errorCode = 0; //QLowEnergyController::NoError
337 final List<BluetoothGattService> services = mBluetoothGatt.getServices();
338 for (BluetoothGattService service: services) {
339 builder.append(service.getUuid().toString()).append(" "); //space is separator
340 }
341 break;
342 default:
343 Log.w(TAG, "Unhandled error code on onServicesDiscovered: " + status);
344 errorCode = status; break; //TODO deal with all errors
345 }
346 leServicesDiscovered(qtObject, errorCode, builder.toString());
347 if (status == BluetoothGatt.GATT_SUCCESS)
348 scheduleMtuExchange();
349 }
350
351 private synchronized void handleOnCharacteristicRead(BluetoothGatt gatt,
352 BluetoothGattCharacteristic characteristic,
353 byte[] value,
354 int status)
355 {
356 int foundHandle = handleForCharacteristic(characteristic);
357 if (foundHandle == -1 || foundHandle >= entries.size() ) {
358 Log.w(TAG, "Cannot find characteristic read request for read notification - handle: " +
359 foundHandle + " size: " + entries.size());
360
361 //unlock the queue for next item
362 pendingJob = null;
363
364 performNextIO();
365 return;
366 }
367
368 boolean requestTimedOut = !handleForTimeout.compareAndSet(
369 modifiedReadWriteHandle(foundHandle, IoJobType.Read),
370 HANDLE_FOR_RESET);
371 if (requestTimedOut) {
372 Log.w(TAG, "Late char read reply after timeout was hit for handle " + foundHandle);
373 // Timeout has hit before this response -> ignore the response
374 // no need to unlock pendingJob -> the timeout has done that already
375 return;
376 }
377
378 GattEntry entry = entries.get(foundHandle);
379 final boolean isServiceDiscoveryRun = !entry.valueKnown;
380 entry.valueKnown = true;
381
382 if (status == BluetoothGatt.GATT_SUCCESS) {
383 // Qt manages handles starting at 1, in Java we use a system starting with 0
384 //TODO avoid sending service uuid -> service handle should be sufficient
385 leCharacteristicRead(qtObject,
386 characteristic.getService().getUuid().toString(),
387 foundHandle + 1, characteristic.getUuid().toString(),
388 characteristic.getProperties(), value);
389 } else {
390 if (isServiceDiscoveryRun) {
391 Log.w(TAG, "onCharacteristicRead during discovery error: " + status);
392
393 Log.d(TAG, "Non-readable characteristic " + characteristic.getUuid() +
394 " for service " + characteristic.getService().getUuid());
395 leCharacteristicRead(qtObject, characteristic.getService().getUuid().toString(),
396 foundHandle + 1, characteristic.getUuid().toString(),
397 characteristic.getProperties(), value);
398 } else {
399 // This must be in sync with QLowEnergyService::CharacteristicReadError
400 final int characteristicReadError = 5;
401 leServiceError(qtObject, foundHandle + 1, characteristicReadError);
402 }
403 }
404
405 if (isServiceDiscoveryRun) {
406
407 // last entry of pending service discovery run -> send discovery finished state update
408 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
409 if (serviceEntry.endHandle == foundHandle)
410 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
411 }
412
413 //unlock the queue for next item
414 pendingJob = null;
415
416 performNextIO();
417 }
418
419 private synchronized void handleOnCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
420 android.bluetooth.BluetoothGattCharacteristic characteristic,
421 byte[] value)
422 {
423 int handle = handleForCharacteristic(characteristic);
424 if (handle == -1) {
425 Log.w(TAG,"onCharacteristicChanged: cannot find handle");
426 return;
427 }
428
429 leCharacteristicChanged(qtObject, handle+1, value);
430 }
431
432 private synchronized void handleOnCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
433 android.bluetooth.BluetoothGattCharacteristic characteristic,
434 int status)
435 {
436 if (status != BluetoothGatt.GATT_SUCCESS)
437 Log.w(TAG, "onCharacteristicWrite: error " + status);
438
439 int handle = handleForCharacteristic(characteristic);
440 if (handle == -1) {
441 Log.w(TAG,"onCharacteristicWrite: cannot find handle");
442 return;
443 }
444
445 boolean requestTimedOut = !handleForTimeout.compareAndSet(
446 modifiedReadWriteHandle(handle, IoJobType.Write),
447 HANDLE_FOR_RESET);
448 if (requestTimedOut) {
449 Log.w(TAG, "Late char write reply after timeout was hit for handle " + handle);
450 // Timeout has hit before this response -> ignore the response
451 // no need to unlock pendingJob -> the timeout has done that already
452 return;
453 }
454
455 int errorCode;
456 //This must be in sync with QLowEnergyService::ServiceError
457 switch (status) {
458 case BluetoothGatt.GATT_SUCCESS:
459 errorCode = 0;
460 break; // NoError
461 default:
462 errorCode = 2;
463 break; // CharacteristicWriteError
464 }
465
466 byte[] value;
467 value = pendingJob.newValue;
468 pendingJob = null;
469
470 leCharacteristicWritten(qtObject, handle+1, value, errorCode);
471 performNextIO();
472 }
473
474 private synchronized void handleOnDescriptorRead(android.bluetooth.BluetoothGatt gatt,
475 android.bluetooth.BluetoothGattDescriptor descriptor,
476 int status, byte[] newValue)
477 {
478 int foundHandle = handleForDescriptor(descriptor);
479 if (foundHandle == -1 || foundHandle >= entries.size() ) {
480 Log.w(TAG, "Cannot find descriptor read request for read notification - handle: " +
481 foundHandle + " size: " + entries.size());
482
483 //unlock the queue for next item
484 pendingJob = null;
485
486 performNextIO();
487 return;
488 }
489
490 boolean requestTimedOut = !handleForTimeout.compareAndSet(
491 modifiedReadWriteHandle(foundHandle, IoJobType.Read),
492 HANDLE_FOR_RESET);
493 if (requestTimedOut) {
494 Log.w(TAG, "Late descriptor read reply after timeout was hit for handle " +
495 foundHandle);
496 // Timeout has hit before this response -> ignore the response
497 // no need to unlock pendingJob -> the timeout has done that already
498 return;
499 }
500
501 GattEntry entry = entries.get(foundHandle);
502 final boolean isServiceDiscoveryRun = !entry.valueKnown;
503 entry.valueKnown = true;
504
505 if (status == BluetoothGatt.GATT_SUCCESS) {
506 //TODO avoid sending service and characteristic uuid -> handles should be sufficient
507 leDescriptorRead(qtObject,
508 descriptor.getCharacteristic().getService().getUuid().toString(),
509 descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1,
510 descriptor.getUuid().toString(), newValue);
511 } else {
512 if (isServiceDiscoveryRun) {
513 // Cannot read but still advertise the fact that we found a descriptor
514 // The value will be empty.
515 Log.w(TAG, "onDescriptorRead during discovery error: " + status);
516 Log.d(TAG, "Non-readable descriptor " + descriptor.getUuid() +
517 " for characteristic " + descriptor.getCharacteristic().getUuid() +
518 " for service " + descriptor.getCharacteristic().getService().getUuid());
519 leDescriptorRead(qtObject,
520 descriptor.getCharacteristic().getService().getUuid().toString(),
521 descriptor.getCharacteristic().getUuid().toString(), foundHandle + 1,
522 descriptor.getUuid().toString(), newValue);
523 } else {
524 // This must be in sync with QLowEnergyService::DescriptorReadError
525 final int descriptorReadError = 6;
526 leServiceError(qtObject, foundHandle + 1, descriptorReadError);
527 }
528
529 }
530
531 if (isServiceDiscoveryRun) {
532 // last entry of pending service discovery run? ->send discovery finished state update
533 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
534 if (serviceEntry.endHandle == foundHandle) {
535 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
536 }
537
538 /* Some devices preset ClientCharacteristicConfiguration descriptors
539 * to enable notifications out of the box. However the additional
540 * BluetoothGatt.setCharacteristicNotification call prevents
541 * automatic notifications from coming through. Hence we manually set them
542 * up here.
543 */
544 if (descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) {
545 byte[] bytearray = newValue;
546 final int value = (bytearray != null && bytearray.length > 0) ? bytearray[0] : 0;
547 // notification or indication bit set?
548 if ((value & 0x03) > 0) {
549 Log.d(TAG, "Found descriptor with automatic notifications.");
550 mBluetoothGatt.setCharacteristicNotification(
551 descriptor.getCharacteristic(), true);
552 }
553 }
554 }
555
556 //unlock the queue for next item
557 pendingJob = null;
558
559 performNextIO();
560 }
561
562 private synchronized void handleOnDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
563 android.bluetooth.BluetoothGattDescriptor descriptor,
564 int status)
565 {
566 if (status != BluetoothGatt.GATT_SUCCESS)
567 Log.w(TAG, "onDescriptorWrite: error " + status);
568
569 int handle = handleForDescriptor(descriptor);
570
571 boolean requestTimedOut = !handleForTimeout.compareAndSet(
572 modifiedReadWriteHandle(handle, IoJobType.Write),
573 HANDLE_FOR_RESET);
574 if (requestTimedOut) {
575 Log.w(TAG, "Late descriptor write reply after timeout was hit for handle " +
576 handle);
577 // Timeout has hit before this response -> ignore the response
578 // no need to unlock pendingJob -> the timeout has done that already
579 return;
580 }
581
582 int errorCode;
583 //This must be in sync with QLowEnergyService::ServiceError
584 switch (status) {
585 case BluetoothGatt.GATT_SUCCESS:
586 errorCode = 0; break; // NoError
587 default:
588 errorCode = 3; break; // DescriptorWriteError
589 }
590
591 byte[] value = pendingJob.newValue;
592 pendingJob = null;
593
594 leDescriptorWritten(qtObject, handle+1, value, errorCode);
595 performNextIO();
596 }
597
598 private synchronized void handleOnMtuChanged(android.bluetooth.BluetoothGatt gatt,
599 int mtu, int status)
600 {
601 int previousMtu = mSupportedMtu;
602 if (status == BluetoothGatt.GATT_SUCCESS) {
603 Log.w(TAG, "MTU changed to " + mtu);
604 mSupportedMtu = mtu;
605 } else {
606 Log.w(TAG, "MTU change error " + status + ". New MTU " + mtu);
607 mSupportedMtu = DEFAULT_MTU;
608 }
609 if (previousMtu != mSupportedMtu)
610 leMtuChanged(qtObject, mSupportedMtu);
611
612 boolean requestTimedOut = !handleForTimeout.compareAndSet(
613 modifiedReadWriteHandle(HANDLE_FOR_MTU_EXCHANGE, IoJobType.Mtu), HANDLE_FOR_RESET);
614 if (requestTimedOut) {
615 Log.w(TAG, "Late mtu reply after timeout was hit");
616 // Timeout has hit before this response -> ignore the response
617 // no need to unlock pendingJob -> the timeout has done that already
618 return;
619 }
620
621 pendingJob = null;
622
623 performNextIO();
624 }
625
626 private synchronized void handleOnReadRemoteRssi(android.bluetooth.BluetoothGatt gatt,
627 int rssi, int status)
628 {
629 Log.d(TAG, "RSSI read callback, rssi: " + rssi + ", status: " + status);
630 leRemoteRssiRead(qtObject, rssi, status == BluetoothGatt.GATT_SUCCESS);
631
632 boolean requestTimedOut = !handleForTimeout.compareAndSet(
633 modifiedReadWriteHandle(HANDLE_FOR_RSSI_READ, IoJobType.Rssi), HANDLE_FOR_RESET);
634 if (requestTimedOut) {
635 Log.w(TAG, "Late RSSI read reply after timeout was hit");
636 // Timeout has hit before this response -> ignore the response
637 // no need to unlock pendingJob -> the timeout has done that already
638 return;
639 }
640 pendingJob = null;
641 performNextIO();
642 }
643
644 /*************************************************************/
645 /* Service Discovery */
646 /*************************************************************/
647
648 private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
649 @Override
650 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
651 super.onConnectionStateChange(gatt, status, newState);
652 handleOnConnectionStateChange(gatt, status, newState);
653 }
654
655 @Override
656 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
657 super.onServicesDiscovered(gatt, status);
658 handleOnServicesDiscovered(gatt, status);
659
660 }
661
662 @Override
663 // API < 33
664 public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
665 android.bluetooth.BluetoothGattCharacteristic characteristic,
666 int status)
667 {
668 super.onCharacteristicRead(gatt, characteristic, status);
669 handleOnCharacteristicRead(gatt, characteristic, characteristic.getValue(), status);
670 }
671
672 @Override
673 // API >= 33
674 public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
675 android.bluetooth.BluetoothGattCharacteristic characteristic,
676 byte[] value,
677 int status)
678 {
679 // Note: here we don't call the super implementation as it calls the old "< API 33"
680 // callback, and the callback would be handled twice
681 handleOnCharacteristicRead(gatt, characteristic, value, status);
682 }
683
684 @Override
685 public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
686 android.bluetooth.BluetoothGattCharacteristic characteristic,
687 int status)
688 {
689 super.onCharacteristicWrite(gatt, characteristic, status);
690 handleOnCharacteristicWrite(gatt, characteristic, status);
691 }
692
693 // API < 33
694 @Override
695 public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
696 android.bluetooth.BluetoothGattCharacteristic characteristic)
697 {
698 super.onCharacteristicChanged(gatt, characteristic);
699 handleOnCharacteristicChanged(gatt, characteristic, characteristic.getValue());
700 }
701
702 // API >= 33
703 @Override
704 public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
705 android.bluetooth.BluetoothGattCharacteristic characteristic,
706 byte[] value)
707 {
708 // Note: here we don't call the super implementation as it calls the old "< API 33"
709 // callback, and the callback would be handled twice
710 handleOnCharacteristicChanged(gatt, characteristic, value);
711 }
712
713 // API < 33
714 @Override
715 public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
716 android.bluetooth.BluetoothGattDescriptor descriptor,
717 int status)
718 {
719 super.onDescriptorRead(gatt, descriptor, status);
720 handleOnDescriptorRead(gatt, descriptor, status, descriptor.getValue());
721 }
722
723 // API >= 33
724 @Override
725 public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
726 android.bluetooth.BluetoothGattDescriptor descriptor,
727 int status,
728 byte[] value)
729 {
730 // Note: here we don't call the super implementation as it calls the old "< API 33"
731 // callback, and the callback would be handled twice
732 handleOnDescriptorRead(gatt, descriptor, status, value);
733 }
734
735 @Override
736 public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
737 android.bluetooth.BluetoothGattDescriptor descriptor,
738 int status)
739 {
740 super.onDescriptorWrite(gatt, descriptor, status);
741 handleOnDescriptorWrite(gatt, descriptor, status);
742 }
743 //TODO currently not supported
744// @Override
745// void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt,
746// int status) {
747// System.out.println("onReliableWriteCompleted");
748// }
749//
750 @Override
751 public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt, int rssi, int status)
752 {
753 super.onReadRemoteRssi(gatt, rssi, status);
754 handleOnReadRemoteRssi(gatt, rssi, status);
755 }
756
757 @Override
758 public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status)
759 {
760 super.onMtuChanged(gatt, mtu, status);
761 handleOnMtuChanged(gatt, mtu, status);
762 }
763 };
764
765 // This function is called from Qt thread
766 synchronized int mtu() {
767 if (mSupportedMtu == -1) {
768 return DEFAULT_MTU;
769 } else {
770 return mSupportedMtu;
771 }
772 }
773
774 // This function is called from Qt thread
775 synchronized boolean readRemoteRssi() {
776 if (mBluetoothGatt == null)
777 return false;
778
779 // Reading of RSSI can sometimes be 'lost' especially if amidst
780 // characteristic reads/writes ('lost' here meaning that there is no callback).
781 // To avoid this schedule the RSSI read in the job queue.
782 ReadWriteJob newJob = new ReadWriteJob();
783 newJob.jobType = IoJobType.Rssi;
784 newJob.entry = null;
785
786 if (!readWriteQueue.add(newJob)) {
787 Log.w(TAG, "Cannot add remote RSSI read to queue" );
788 return false;
789 }
790
791 performNextIOThreaded();
792 return true;
793 }
794
795 // This function is called from Qt thread
796 synchronized boolean connect() {
797 BluetoothDevice mRemoteGattDevice;
798
799 if (mBluetoothAdapter == null) {
800 Log.w(TAG, "Cannot connect, no bluetooth adapter");
801 return false;
802 }
803
804 try {
805 mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
806 } catch (IllegalArgumentException ex) {
807 Log.w(TAG, "Remote address is not valid: " + mRemoteGattAddress);
808 return false;
809 }
810
811 /* The required connectGatt function is already available in SDK v26, but Android 8.0
812 * contains a race condition in the Changed callback such that it can return the value that
813 * was written. This is fixed in Android 8.1, which matches SDK v27. */
814 if (Build.VERSION.SDK_INT >= 27) {
815 HandlerThread handlerThread = new HandlerThread("QtBluetoothLEHandlerThread");
816 handlerThread.start();
817 mHandler = new Handler(handlerThread.getLooper());
818
819 Class[] args = new Class[6];
820 args[0] = android.content.Context.class;
821 args[1] = boolean.class;
822 args[2] = android.bluetooth.BluetoothGattCallback.class;
823 args[3] = int.class;
824 args[4] = int.class;
825 args[5] = android.os.Handler.class;
826
827 try {
828 Method connectMethod = mRemoteGattDevice.getClass().getDeclaredMethod("connectGatt", args);
829 if (connectMethod != null) {
830 mBluetoothGatt = (BluetoothGatt) connectMethod.invoke(mRemoteGattDevice, qtContext, false,
831 gattCallback, 2 /* TRANSPORT_LE */, 1 /*BluetoothDevice.PHY_LE_1M*/, mHandler);
832 Log.w(TAG, "Using Android v26 BluetoothDevice.connectGatt()");
833 }
834 } catch (Exception ex) {
835 Log.w(TAG, "connectGatt() v26 not available");
836 ex.printStackTrace();
837 }
838
839 if (mBluetoothGatt == null) {
840 mHandler.getLooper().quitSafely();
841 mHandler = null;
842 }
843 }
844
845 if (mBluetoothGatt == null) {
846 try {
847 //This API element is currently: greylist-max-o (API level 27), reflection, allowed
848 //It may change in the future
849 Class[] constr_args = new Class[5];
850 constr_args[0] = android.bluetooth.BluetoothGattService.class;
851 constr_args[1] = java.util.UUID.class;
852 constr_args[2] = int.class;
853 constr_args[3] = int.class;
854 constr_args[4] = int.class;
855 mCharacteristicConstructor = BluetoothGattCharacteristic.class.getDeclaredConstructor(constr_args);
856 mCharacteristicConstructor.setAccessible(true);
857 } catch (NoSuchMethodException ex) {
858 Log.w(TAG, "Unable get characteristic constructor. Buffer race condition are possible");
859 /* For some reason we don't get the private BluetoothGattCharacteristic ctor.
860 This means that we cannot protect ourselves from issues where concurrent
861 read and write operations on the same char can overwrite each others buffer.
862 Nevertheless we continue with best effort.
863 */
864 }
865 try {
866 mBluetoothGatt =
867 mRemoteGattDevice.connectGatt(qtContext, false,
868 gattCallback, 2 /* TRANSPORT_LE */);
869 } catch (IllegalArgumentException ex) {
870 Log.w(TAG, "Gatt connection failed");
871 ex.printStackTrace();
872 }
873 }
874 return mBluetoothGatt != null;
875 }
876
877 // This function is called from Qt thread
878 synchronized void disconnect() {
879 if (mBluetoothGatt == null)
880 return;
881
882 mBluetoothGatt.disconnect();
883 }
884
885 // This function is called from Qt thread
886 synchronized boolean discoverServices()
887 {
888 return mBluetoothGatt != null && mBluetoothGatt.discoverServices();
889 }
890
891 private enum GattEntryType
892 {
893 Service, Characteristic, CharacteristicValue, Descriptor
895 private class GattEntry
896 {
898 boolean valueKnown = false;
899 BluetoothGattService service = null;
900 BluetoothGattCharacteristic characteristic = null;
901 BluetoothGattDescriptor descriptor = null;
902 /*
903 * endHandle defined for GattEntryType.Service and GattEntryType.CharacteristicValue
904 * If the type is service this is the value of the last Gatt entry belonging to the very
905 * same service. If the type is a char value it is the entries index inside
906 * the "entries" list.
907 */
908 int endHandle = -1;
909 // pointer back to the handle that describes the service that this GATT entry belongs to
910 int associatedServiceHandle;
911 }
912
913 private enum IoJobType
914 {
915 Read, Write, Mtu,
917 // a skipped read is a read which is not executed
918 // introduced in Qt 6.2 to skip reads without changing service discovery logic
920
921 private class ReadWriteJob
922 {
923 GattEntry entry;
924 byte[] newValue;
925 int requestedWriteType;
926 IoJobType jobType;
927 }
928
929 // service uuid -> service handle mapping (there can be more than one service with same uuid)
930 private final Hashtable<UUID, List<Integer>> uuidToEntry = new Hashtable<UUID, List<Integer>>(100);
931 // index into array is equivalent to handle id
932 private final ArrayList<GattEntry> entries = new ArrayList<GattEntry>(100);
933 //backlog of to be discovered services
934 private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>();
935
936
937 private final LinkedList<ReadWriteJob> readWriteQueue = new LinkedList<ReadWriteJob>();
938 private ReadWriteJob pendingJob;
939
940 /*
941 Internal helper function
942 Returns the handle id for the given characteristic; otherwise returns -1.
943
944 Note that this is the Java handle. The Qt handle is the Java handle +1.
945 */
946 private int handleForCharacteristic(BluetoothGattCharacteristic characteristic)
947 {
948 if (characteristic == null)
949 return -1;
950
951 List<Integer> handles = uuidToEntry.get(characteristic.getService().getUuid());
952 if (handles == null || handles.isEmpty())
953 return -1;
954
955 //TODO for now we assume we always want the first service in case of uuid collision
956 int serviceHandle = handles.get(0);
957
958 try {
959 GattEntry entry;
960 for (int i = serviceHandle+1; i < entries.size(); i++) {
961 entry = entries.get(i);
962 if (entry == null)
963 continue;
964
965 switch (entry.type) {
966 case Descriptor:
967 case CharacteristicValue:
968 continue;
969 case Service:
970 break;
971 case Characteristic:
972 if (entry.characteristic == characteristic)
973 return i;
974 break;
975 }
976 }
977 } catch (IndexOutOfBoundsException ex) { /*nothing*/ }
978 return -1;
979 }
980
981 /*
982 Internal helper function
983 Returns the handle id for the given descriptor; otherwise returns -1.
984
985 Note that this is the Java handle. The Qt handle is the Java handle +1.
986 */
987 private int handleForDescriptor(BluetoothGattDescriptor descriptor)
988 {
989 if (descriptor == null)
990 return -1;
991
992 List<Integer> handles = uuidToEntry.get(descriptor.getCharacteristic().getService().getUuid());
993 if (handles == null || handles.isEmpty())
994 return -1;
995
996 //TODO for now we assume we always want the first service in case of uuid collision
997 int serviceHandle = handles.get(0);
998
999 try {
1000 GattEntry entry;
1001 for (int i = serviceHandle+1; i < entries.size(); i++) {
1002 entry = entries.get(i);
1003 if (entry == null)
1004 continue;
1005
1006 switch (entry.type) {
1007 case Characteristic:
1008 case CharacteristicValue:
1009 continue;
1010 case Service:
1011 break;
1012 case Descriptor:
1013 if (entry.descriptor == descriptor)
1014 return i;
1015 break;
1016 }
1017 }
1018 } catch (IndexOutOfBoundsException ignored) { }
1019 return -1;
1020 }
1021
1022 // This function is called from Qt thread (indirectly)
1023 private void populateHandles()
1024 {
1025 // We introduce the notion of artificial handles. While GATT handles
1026 // are not exposed on Android they help to quickly identify GATT attributes
1027 // on the C++ side. The Qt Api will not expose the handles
1028 GattEntry entry = null;
1029 List<BluetoothGattService> services = mBluetoothGatt.getServices();
1030 for (BluetoothGattService service: services) {
1031 GattEntry serviceEntry = new GattEntry();
1032 serviceEntry.type = GattEntryType.Service;
1033 serviceEntry.service = service;
1034 entries.add(serviceEntry);
1035
1036 // remember handle for the service for later update
1037 int serviceHandle = entries.size() - 1;
1038 //point to itself -> mostly done for consistence reasons with other entries
1039 serviceEntry.associatedServiceHandle = serviceHandle;
1040
1041 //some devices may have more than one service with the same uuid
1042 List<Integer> old = uuidToEntry.get(service.getUuid());
1043 if (old == null)
1044 old = new ArrayList<Integer>();
1045 old.add(entries.size()-1);
1046 uuidToEntry.put(service.getUuid(), old);
1047
1048 // add all characteristics
1049 List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
1050 for (BluetoothGattCharacteristic characteristic: charList) {
1051 entry = new GattEntry();
1052 entry.type = GattEntryType.Characteristic;
1053 entry.characteristic = characteristic;
1054 entry.associatedServiceHandle = serviceHandle;
1055 //entry.endHandle = .. undefined
1056 entries.add(entry);
1057
1058 // this emulates GATT value attributes
1059 entry = new GattEntry();
1060 entry.type = GattEntryType.CharacteristicValue;
1061 entry.associatedServiceHandle = serviceHandle;
1062 entry.endHandle = entries.size(); // special case -> current index in entries list
1063 entries.add(entry);
1064
1065 // add all descriptors
1066 List<BluetoothGattDescriptor> descList = characteristic.getDescriptors();
1067 for (BluetoothGattDescriptor desc: descList) {
1068 entry = new GattEntry();
1069 entry.type = GattEntryType.Descriptor;
1070 entry.descriptor = desc;
1071 entry.associatedServiceHandle = serviceHandle;
1072 //entry.endHandle = .. undefined
1073 entries.add(entry);
1074 }
1075 }
1076
1077 // update endHandle of current service
1078 serviceEntry.endHandle = entries.size() - 1;
1079 }
1080
1081 entries.trimToSize();
1082 }
1083
1084 private void resetData()
1085 {
1086 uuidToEntry.clear();
1087 entries.clear();
1088 servicesToBeDiscovered.clear();
1089
1090 // kill all timeout handlers
1091 timeoutHandler.removeCallbacksAndMessages(null);
1092 handleForTimeout.set(HANDLE_FOR_RESET);
1093
1094 readWriteQueue.clear();
1095 pendingJob = null;
1096 }
1097
1098 // This function is called from Qt thread
1099 synchronized boolean discoverServiceDetails(String serviceUuid, boolean fullDiscovery)
1100 {
1101 Log.d(TAG, "Discover service details for: " + serviceUuid + ", fullDiscovery: "
1102 + fullDiscovery + ", BluetoothGatt: " + (mBluetoothGatt != null));
1103 try {
1104 if (mBluetoothGatt == null)
1105 return false;
1106
1107 if (entries.isEmpty())
1108 populateHandles();
1109
1110 GattEntry entry;
1111 int serviceHandle;
1112 try {
1113 UUID service = UUID.fromString(serviceUuid);
1114 List<Integer> handles = uuidToEntry.get(service);
1115 if (handles == null || handles.isEmpty()) {
1116 Log.w(TAG, "Unknown service uuid for current device: " + service.toString());
1117 return false;
1118 }
1119
1120 //TODO for now we assume we always want the first service in case of uuid collision
1121 serviceHandle = handles.get(0);
1122 entry = entries.get(serviceHandle);
1123 if (entry == null) {
1124 Log.w(TAG, "Service with UUID " + service.toString() + " not found");
1125 return false;
1126 }
1127 } catch (IllegalArgumentException ex) {
1128 //invalid UUID string passed
1129 Log.w(TAG, "Cannot parse given UUID");
1130 return false;
1131 }
1132
1133 if (entry.type != GattEntryType.Service) {
1134 Log.w(TAG, "Given UUID is not a service UUID: " + serviceUuid);
1135 return false;
1136 }
1137
1138 // current service already discovered or under investigation
1139 if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) {
1140 Log.w(TAG, "Service already known or to be discovered");
1141 return true;
1142 }
1143
1144 servicesToBeDiscovered.add(serviceHandle);
1145 scheduleServiceDetailDiscovery(serviceHandle, fullDiscovery);
1146 performNextIOThreaded();
1147 } catch (Exception ex) {
1148 ex.printStackTrace();
1149 return false;
1150 }
1151
1152 return true;
1153 }
1154
1155 /*
1156 Returns the uuids of the services included by the given service. Otherwise returns null.
1157 This function is called from Qt thread
1158 */
1159 synchronized String includedServices(String serviceUuid)
1160 {
1161 if (mBluetoothGatt == null)
1162 return null;
1163
1164 UUID uuid;
1165 try {
1166 uuid = UUID.fromString(serviceUuid);
1167 } catch (Exception ex) {
1168 ex.printStackTrace();
1169 return null;
1170 }
1171
1172 //TODO Breaks in case of two services with same uuid
1173 BluetoothGattService service = mBluetoothGatt.getService(uuid);
1174 if (service == null)
1175 return null;
1176
1177 final List<BluetoothGattService> includes = service.getIncludedServices();
1178 if (includes.isEmpty())
1179 return null;
1180
1181 StringBuilder builder = new StringBuilder();
1182 for (BluetoothGattService includedService: includes) {
1183 builder.append(includedService.getUuid().toString()).append(" "); //space is separator
1184 }
1185
1186 return builder.toString();
1187 }
1188
1189 private synchronized void finishCurrentServiceDiscovery(int handleDiscoveredService)
1190 {
1191 Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService);
1192 GattEntry discoveredService = entries.get(handleDiscoveredService);
1193 discoveredService.valueKnown = true;
1194 try {
1195 servicesToBeDiscovered.removeFirst();
1196 } catch (NoSuchElementException ex) {
1197 Log.w(TAG, "Expected queued service but didn't find any");
1198 }
1199
1200 leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(),
1201 handleDiscoveredService + 1, discoveredService.endHandle + 1);
1202 }
1203
1204 // Executes under "this" client mutex. Returns true
1205 // if no actual MTU exchange is initiated
1206 private boolean executeMtuExchange()
1207 {
1208 if (mBluetoothGatt.requestMtu(MAX_MTU)) {
1209 Log.w(TAG, "MTU change initiated");
1210 return false;
1211 } else {
1212 Log.w(TAG, "MTU change request failed");
1213 }
1214
1215 Log.w(TAG, "Assuming default MTU value of 23 bytes");
1216 mSupportedMtu = DEFAULT_MTU;
1217 return true;
1218 }
1219
1220 private boolean executeRemoteRssiRead()
1221 {
1222 if (mBluetoothGatt.readRemoteRssi()) {
1223 Log.d(TAG, "RSSI read initiated");
1224 return false;
1225 }
1226 Log.w(TAG, "Initiating remote RSSI read failed");
1227 leRemoteRssiRead(qtObject, 0, false);
1228 return true;
1229 }
1230
1231 /*
1232 * Already executed in GattCallback so executed by the HandlerThread. No need to
1233 * post it to the Hander.
1234 */
1235 private void scheduleMtuExchange() {
1236 ReadWriteJob newJob = new ReadWriteJob();
1237 newJob.jobType = IoJobType.Mtu;
1238 newJob.entry = null;
1239
1240 readWriteQueue.add(newJob);
1241
1242 performNextIO();
1243 }
1244
1245 /*
1246 Internal Helper function for discoverServiceDetails()
1247
1248 Adds all Gatt entries for the given service to the readWriteQueue to be discovered.
1249 This function only ever adds read requests to the queue.
1250
1251 */
1252 private void scheduleServiceDetailDiscovery(int serviceHandle, boolean fullDiscovery)
1253 {
1254 GattEntry serviceEntry = entries.get(serviceHandle);
1255 final int endHandle = serviceEntry.endHandle;
1256
1257 if (serviceHandle == endHandle) {
1258 Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover");
1259 finishCurrentServiceDiscovery(serviceHandle);
1260 return;
1261 }
1262
1263 // serviceHandle + 1 -> ignore service handle itself
1264 for (int i = serviceHandle + 1; i <= endHandle; i++) {
1265 GattEntry entry = entries.get(i);
1266
1267 if (entry.type == GattEntryType.Service) {
1268 // should not really happen unless endHandle is wrong
1269 Log.w(TAG, "scheduleServiceDetailDiscovery: wrong endHandle");
1270 return;
1271 }
1272
1273 ReadWriteJob newJob = new ReadWriteJob();
1274 newJob.entry = entry;
1275 if (fullDiscovery) {
1276 newJob.jobType = IoJobType.Read;
1277 } else {
1278 newJob.jobType = IoJobType.SkippedRead;
1279 }
1280
1281 final boolean result = readWriteQueue.add(newJob);
1282 if (!result)
1283 Log.w(TAG, "Cannot add service discovery job for " + serviceEntry.service.getUuid()
1284 + " on item " + entry.type);
1285 }
1286 }
1287
1288 /*************************************************************/
1289 /* Write Characteristics */
1290 /* This function is called from Qt thread */
1291 /*************************************************************/
1292
1293 synchronized boolean writeCharacteristic(int charHandle, byte[] newValue,
1294 int writeMode)
1295 {
1296 if (mBluetoothGatt == null)
1297 return false;
1298
1299 GattEntry entry;
1300 try {
1301 entry = entries.get(charHandle-1); //Qt always uses handles+1
1302 } catch (IndexOutOfBoundsException ex) {
1303 ex.printStackTrace();
1304 return false;
1305 }
1306
1307 ReadWriteJob newJob = new ReadWriteJob();
1308 newJob.newValue = newValue;
1309 newJob.entry = entry;
1310 newJob.jobType = IoJobType.Write;
1311
1312 // writeMode must be in sync with QLowEnergyService::WriteMode
1313 switch (writeMode) {
1314 case 1: //WriteWithoutResponse
1315 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
1316 break;
1317 case 2: //WriteSigned
1318 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_SIGNED;
1319 break;
1320 default:
1321 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
1322 break;
1323 }
1324
1325 boolean result;
1326 result = readWriteQueue.add(newJob);
1327
1328 if (!result) {
1329 Log.w(TAG, "Cannot add characteristic write request for " + charHandle + " to queue" );
1330 return false;
1331 }
1332
1333 performNextIOThreaded();
1334 return true;
1335 }
1336
1337 /*************************************************************/
1338 /* Write Descriptors */
1339 /* This function is called from Qt thread */
1340 /*************************************************************/
1341
1342 synchronized boolean writeDescriptor(int descHandle, byte[] newValue)
1343 {
1344 if (mBluetoothGatt == null)
1345 return false;
1346
1347 GattEntry entry;
1348 try {
1349 entry = entries.get(descHandle-1); //Qt always uses handles+1
1350 } catch (IndexOutOfBoundsException ex) {
1351 ex.printStackTrace();
1352 return false;
1353 }
1354
1355 ReadWriteJob newJob = new ReadWriteJob();
1356 newJob.newValue = newValue;
1357 newJob.entry = entry;
1358 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
1359 newJob.jobType = IoJobType.Write;
1360
1361 boolean result;
1362 result = readWriteQueue.add(newJob);
1363
1364 if (!result) {
1365 Log.w(TAG, "Cannot add descriptor write request for " + descHandle + " to queue" );
1366 return false;
1367 }
1368
1369 performNextIOThreaded();
1370 return true;
1371 }
1372
1373 /*************************************************************/
1374 /* Read Characteristics */
1375 /* This function is called from Qt thread */
1376 /*************************************************************/
1377
1378 synchronized boolean readCharacteristic(int charHandle)
1379 {
1380 if (mBluetoothGatt == null)
1381 return false;
1382
1383 GattEntry entry;
1384 try {
1385 entry = entries.get(charHandle-1); //Qt always uses handles+1
1386 } catch (IndexOutOfBoundsException ex) {
1387 ex.printStackTrace();
1388 return false;
1389 }
1390
1391 ReadWriteJob newJob = new ReadWriteJob();
1392 newJob.entry = entry;
1393 newJob.jobType = IoJobType.Read;
1394
1395 boolean result;
1396 result = readWriteQueue.add(newJob);
1397
1398 if (!result) {
1399 Log.w(TAG, "Cannot add characteristic read request for " + charHandle + " to queue" );
1400 return false;
1401 }
1402
1403 performNextIOThreaded();
1404 return true;
1405 }
1406
1407 // This function is called from Qt thread
1408 synchronized boolean readDescriptor(int descHandle)
1409 {
1410 if (mBluetoothGatt == null)
1411 return false;
1412
1413 GattEntry entry;
1414 try {
1415 entry = entries.get(descHandle-1); //Qt always uses handles+1
1416 } catch (IndexOutOfBoundsException ex) {
1417 ex.printStackTrace();
1418 return false;
1419 }
1420
1421 ReadWriteJob newJob = new ReadWriteJob();
1422 newJob.entry = entry;
1423 newJob.jobType = IoJobType.Read;
1424
1425 boolean result;
1426 result = readWriteQueue.add(newJob);
1427
1428 if (!result) {
1429 Log.w(TAG, "Cannot add descriptor read request for " + descHandle + " to queue" );
1430 return false;
1431 }
1432
1433 performNextIOThreaded();
1434 return true;
1435 }
1436
1437 // Called by TimeoutRunnable if the current I/O job timed out.
1438 // By the time we reach this point the handleForTimeout counter has already been reset
1439 // and the regular responses will be blocked off.
1440 private synchronized void interruptCurrentIO(int handle)
1441 {
1442 //unlock the queue for next item
1443 pendingJob = null;
1444
1445 performNextIOThreaded();
1446
1447 if (handle == HANDLE_FOR_MTU_EXCHANGE || handle == HANDLE_FOR_RSSI_READ)
1448 return;
1449
1450 try {
1451 GattEntry entry = entries.get(handle);
1452 if (entry == null)
1453 return;
1454 if (entry.valueKnown)
1455 return;
1456 entry.valueKnown = true;
1457
1458 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
1459 if (serviceEntry != null && serviceEntry.endHandle == handle)
1460 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
1461 } catch (IndexOutOfBoundsException outOfBounds) {
1462 Log.w(TAG, "interruptCurrentIO(): Unknown gatt entry, index: "
1463 + handle + " size: " + entries.size());
1464 }
1465 }
1466
1467 /*
1468 Wrapper around performNextIO() ensuring that performNextIO() is executed inside
1469 the mHandler/mHandlerThread if it exists.
1470 */
1471 private void performNextIOThreaded()
1472 {
1473 if (mHandler != null) {
1474 mHandler.post(new Runnable() {
1475 @Override
1476 public void run() {
1477 performNextIO();
1478 }
1479 });
1480 } else {
1481 performNextIO();
1482 }
1483 }
1484
1485 /*
1486 The queuing is required because two writeCharacteristic/writeDescriptor calls
1487 cannot execute at the same time. The second write must happen after the
1488 previous write has finished with on(Characteristic|Descriptor)Write().
1489 */
1490 private synchronized void performNextIO()
1491 {
1492 Log.d(TAG, "Perform next BTLE IO, job queue size: " + readWriteQueue.size()
1493 + ", a job is pending: " + (pendingJob != null) + ", BluetoothGatt: "
1494 + (mBluetoothGatt != null));
1495
1496 if (mBluetoothGatt == null)
1497 return;
1498
1499 boolean skip = false;
1500 final ReadWriteJob nextJob;
1501 int handle = HANDLE_FOR_RESET;
1502
1503 if (readWriteQueue.isEmpty() || pendingJob != null)
1504 return;
1505
1506 nextJob = readWriteQueue.remove();
1507 // MTU requests and RSSI reads are special cases
1508 if (nextJob.jobType == IoJobType.Mtu) {
1509 handle = HANDLE_FOR_MTU_EXCHANGE;
1510 } else if (nextJob.jobType == IoJobType.Rssi) {
1511 handle = HANDLE_FOR_RSSI_READ;
1512 } else {
1513 switch (nextJob.entry.type) {
1514 case Characteristic:
1515 handle = handleForCharacteristic(nextJob.entry.characteristic);
1516 break;
1517 case Descriptor:
1518 handle = handleForDescriptor(nextJob.entry.descriptor);
1519 break;
1520 case CharacteristicValue:
1521 handle = nextJob.entry.endHandle;
1522 default:
1523 break;
1524 }
1525 }
1526
1527 // timeout handler and handleForTimeout atomic must be setup before
1528 // executing the request. Sometimes the callback is quicker than executing the
1529 // remainder of this function. Therefore enable the atomic early
1530 timeoutHandler.removeCallbacksAndMessages(null); // remove any timeout handlers
1531 handleForTimeout.set(modifiedReadWriteHandle(handle, nextJob.jobType));
1532
1533 switch (nextJob.jobType) {
1534 case Read:
1535 skip = executeReadJob(nextJob);
1536 break;
1537 case SkippedRead:
1538 skip = true;
1539 break;
1540 case Write:
1541 skip = executeWriteJob(nextJob);
1542 break;
1543 case Mtu:
1544 skip = executeMtuExchange();
1545 case Rssi:
1546 skip = executeRemoteRssiRead();
1547 break;
1548 }
1549
1550 if (skip) {
1551 handleForTimeout.set(HANDLE_FOR_RESET); // not a pending call -> release atomic
1552 } else {
1553 pendingJob = nextJob;
1554 timeoutHandler.postDelayed(new TimeoutRunnable(
1555 modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT);
1556 }
1557
1558 if (nextJob.jobType != IoJobType.Mtu && nextJob.jobType != IoJobType.Rssi) {
1559 Log.d(TAG, "Performing queued job, handle: " + handle + " " + nextJob.jobType + " (" +
1560 (nextJob.requestedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) +
1561 ") ValueKnown: " + nextJob.entry.valueKnown + " Skipping: " + skip +
1562 " " + nextJob.entry.type);
1563 }
1564
1565 GattEntry entry = nextJob.entry;
1566
1567 if (skip) {
1568 /*
1569 BluetoothGatt.[read|write][Characteristic|Descriptor]() immediately
1570 return in cases where meta data doesn't match the intended action
1571 (e.g. trying to write to read-only char). When this happens
1572 we have to report an error back to Qt. The error report is not required during
1573 the initial service discovery though.
1574 */
1575 if (handle > HANDLE_FOR_RESET) {
1576 // during service discovery we do not report error but emit characteristicRead()
1577 // any other time a failure emits serviceError() signal
1578
1579 final boolean isServiceDiscovery = !entry.valueKnown;
1580
1581 if (isServiceDiscovery) {
1582 entry.valueKnown = true;
1583 switch (entry.type) {
1584 case Characteristic:
1585 Log.d(TAG,
1586 nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
1587 + " characteristic " + entry.characteristic.getUuid()
1588 + " for service " + entry.characteristic.getService().getUuid());
1589 leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(),
1590 handle + 1, entry.characteristic.getUuid().toString(),
1591 entry.characteristic.getProperties(), null);
1592 break;
1593 case Descriptor:
1594 Log.d(TAG,
1595 nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
1596 + " descriptor " + entry.descriptor.getUuid()
1597 + " for service/char " + entry.descriptor.getCharacteristic().getService().getUuid()
1598 + "/" + entry.descriptor.getCharacteristic().getUuid());
1599 leDescriptorRead(qtObject,
1600 entry.descriptor.getCharacteristic().getService().getUuid().toString(),
1601 entry.descriptor.getCharacteristic().getUuid().toString(),
1602 handle + 1, entry.descriptor.getUuid().toString(),
1603 null);
1604 break;
1605 case CharacteristicValue:
1606 // for more details see scheduleServiceDetailDiscovery(int, boolean)
1607 break;
1608 case Service:
1609 Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen.");
1610 break;
1611 }
1612
1613 // last entry of current discovery run?
1614 try {
1615 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
1616 if (serviceEntry.endHandle == handle)
1617 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
1618 } catch (IndexOutOfBoundsException outOfBounds) {
1619 Log.w(TAG, "performNextIO(): Unknown service for entry, index: "
1620 + entry.associatedServiceHandle + " size: " + entries.size());
1621 }
1622 } else {
1623 int errorCode = 0;
1624
1625 // The error codes below must be in sync with QLowEnergyService::ServiceError
1626 if (nextJob.jobType == IoJobType.Read) {
1627 errorCode = (entry.type == GattEntryType.Characteristic) ?
1628 5 : 6; // CharacteristicReadError : DescriptorReadError
1629 } else {
1630 errorCode = (entry.type == GattEntryType.Characteristic) ?
1631 2 : 3; // CharacteristicWriteError : DescriptorWriteError
1632 }
1633
1634 leServiceError(qtObject, handle + 1, errorCode);
1635 }
1636 }
1637
1638 performNextIO();
1639 }
1640 }
1641
1642 private BluetoothGattCharacteristic cloneChararacteristic(BluetoothGattCharacteristic other) {
1643 try {
1644 return (BluetoothGattCharacteristic) mCharacteristicConstructor.newInstance(other.getService(),
1645 other.getUuid(), other.getInstanceId(), other.getProperties(), other.getPermissions());
1646 } catch (Exception ex) {
1647 Log.w(TAG, "Cloning characteristic failed!" + ex);
1648 return null;
1649 }
1650 }
1651
1652 // Returns true if nextJob should be skipped.
1653 private boolean executeWriteJob(ReadWriteJob nextJob)
1654 {
1655 boolean result;
1656 switch (nextJob.entry.type) {
1657 case Characteristic:
1658 if (Build.VERSION.SDK_INT >= 33) {
1659 int writeResult = mBluetoothGatt.writeCharacteristic(
1660 nextJob.entry.characteristic, nextJob.newValue, nextJob.requestedWriteType);
1661 return (writeResult != BluetoothStatusCodes.SUCCESS);
1662 }
1663 if (mHandler != null || mCharacteristicConstructor == null) {
1664 if (nextJob.entry.characteristic.getWriteType() != nextJob.requestedWriteType) {
1665 nextJob.entry.characteristic.setWriteType(nextJob.requestedWriteType);
1666 }
1667 result = nextJob.entry.characteristic.setValue(nextJob.newValue);
1668 return !result || !mBluetoothGatt.writeCharacteristic(nextJob.entry.characteristic);
1669 } else {
1670 BluetoothGattCharacteristic orig = nextJob.entry.characteristic;
1671 BluetoothGattCharacteristic tmp = cloneChararacteristic(orig);
1672 if (tmp == null)
1673 return true;
1674 tmp.setWriteType(nextJob.requestedWriteType);
1675 return !tmp.setValue(nextJob.newValue) || !mBluetoothGatt.writeCharacteristic(tmp);
1676 }
1677 case Descriptor:
1678 if (nextJob.entry.descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) {
1679 /*
1680 For some reason, Android splits characteristic notifications
1681 into two operations. BluetoothGatt.enableCharacteristicNotification
1682 ensures the local Bluetooth stack forwards the notifications. In addition,
1683 BluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
1684 must be written to the peripheral.
1685 */
1686
1687
1688 /* There is no documentation on indication behavior. The assumption is
1689 that when indication or notification are requested we call
1690 BluetoothGatt.setCharacteristicNotification. Furthermore it is assumed
1691 indications are send via onCharacteristicChanged too and Android itself
1692 will do the confirmation required for an indication as per
1693 Bluetooth spec Vol 3, Part G, 4.11 . If neither of the two bits are set
1694 we disable the signals.
1695 */
1696 boolean enableNotifications = false;
1697 int value = (nextJob.newValue[0] & 0xff);
1698 // first or second bit must be set
1699 if (((value & 0x1) == 1) || (((value >> 1) & 0x1) == 1)) {
1700 enableNotifications = true;
1701 }
1702
1703 result = mBluetoothGatt.setCharacteristicNotification(
1704 nextJob.entry.descriptor.getCharacteristic(), enableNotifications);
1705 if (!result) {
1706 Log.w(TAG, "Cannot set characteristic notification");
1707 //we continue anyway to ensure that we write the requested value
1708 //to the device
1709 }
1710
1711 Log.d(TAG, "Enable notifications: " + enableNotifications);
1712 }
1713
1714 if (Build.VERSION.SDK_INT >= 33) {
1715 int writeResult = mBluetoothGatt.writeDescriptor(
1716 nextJob.entry.descriptor, nextJob.newValue);
1717 return (writeResult != BluetoothStatusCodes.SUCCESS);
1718 }
1719 result = nextJob.entry.descriptor.setValue(nextJob.newValue);
1720 if (!result || !mBluetoothGatt.writeDescriptor(nextJob.entry.descriptor))
1721 return true;
1722
1723 break;
1724 case Service:
1725 case CharacteristicValue:
1726 return true;
1727 }
1728 return false;
1729 }
1730
1731 // Returns true if nextJob should be skipped.
1732 private boolean executeReadJob(ReadWriteJob nextJob)
1733 {
1734 boolean result;
1735 switch (nextJob.entry.type) {
1736 case Characteristic:
1737 try {
1738 result = mBluetoothGatt.readCharacteristic(nextJob.entry.characteristic);
1739 } catch (java.lang.SecurityException se) {
1740 // QTBUG-59917 -> HID services cause problems since Android 5.1
1741 se.printStackTrace();
1742 result = false;
1743 }
1744 if (!result)
1745 return true; // skip
1746 break;
1747 case Descriptor:
1748 try {
1749 result = mBluetoothGatt.readDescriptor(nextJob.entry.descriptor);
1750 } catch (java.lang.SecurityException se) {
1751 // QTBUG-59917 -> HID services cause problems since Android 5.1
1752 se.printStackTrace();
1753 result = false;
1754 }
1755 if (!result)
1756 return true; // skip
1757 break;
1758 case Service:
1759 return true;
1760 case CharacteristicValue:
1761 return true; //skip
1762 }
1763 return false;
1764 }
1765
1766 /*
1767 * Modifies and returns the given \a handle such that the job
1768 * \a type is encoded into the returned handle. Hereby we take advantage of the fact that
1769 * a Bluetooth Low Energy handle is only 16 bit. The handle will be the bottom two bytes
1770 * and the job type will be in the top 2 bytes.
1771 *
1772 * top 2 bytes
1773 * - 0x01 -> Read Job
1774 * - 0x02 -> Write Job
1775 *
1776 * This is done in connection with handleForTimeout and assists in the process of
1777 * detecting accidental interruption by the timeout handler.
1778 * If two requests for the same handle are scheduled behind each other there is the
1779 * theoretical chance that the first request comes back normally while the second request
1780 * is interrupted by the timeout handler. This risk still exists but this function ensures that
1781 * at least back to back requests of differing types cannot affect each other via the timeout
1782 * handler.
1783 */
1784 private int modifiedReadWriteHandle(int handle, IoJobType type)
1785 {
1786 int modifiedHandle = handle;
1787 // ensure we have 16bit handle only
1788 if (handle > 0xFFFF)
1789 Log.w(TAG, "Invalid handle");
1790
1791 modifiedHandle = (modifiedHandle & 0xFFFF);
1792
1793 switch (type) {
1794 case Write:
1795 modifiedHandle = (modifiedHandle | 0x00010000);
1796 break;
1797 case Read:
1798 modifiedHandle = (modifiedHandle | 0x00020000);
1799 break;
1800 case Mtu:
1801 modifiedHandle = HANDLE_FOR_MTU_EXCHANGE;
1802 break;
1803 case Rssi:
1804 modifiedHandle = HANDLE_FOR_RSSI_READ;
1805 break;
1806 }
1807
1808 return modifiedHandle;
1809 }
1810
1811 // This function is called from Qt thread
1812 synchronized boolean requestConnectionUpdatePriority(double minimalInterval)
1813 {
1814 if (mBluetoothGatt == null)
1815 return false;
1816
1817 int requestPriority = 0; // BluetoothGatt.CONNECTION_PRIORITY_BALANCED
1818 if (minimalInterval < 30)
1819 requestPriority = 1; // BluetoothGatt.CONNECTION_PRIORITY_HIGH
1820 else if (minimalInterval > 100)
1821 requestPriority = 2; //BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER
1822
1823 try {
1824 return mBluetoothGatt.requestConnectionPriority(requestPriority);
1825 } catch (IllegalArgumentException ex) {
1826 Log.w(TAG, "Connection update priority out of range: " + requestPriority);
1827 return false;
1828 }
1829 }
1830
1831 native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState);
1832 native void leMtuChanged(long qtObject, int mtu);
1833 native void leRemoteRssiRead(long qtObject, int rssi, boolean success);
1834 native void leServicesDiscovered(long qtObject, int errorCode, String uuidList);
1835 native void leServiceDetailDiscoveryFinished(long qtObject, final String serviceUuid,
1836 int startHandle, int endHandle);
1837 native void leCharacteristicRead(long qtObject, String serviceUuid,
1838 int charHandle, String charUuid,
1839 int properties, byte[] data);
1840 native void leDescriptorRead(long qtObject, String serviceUuid, String charUuid,
1841 int descHandle, String descUuid, byte[] data);
1842 native void leCharacteristicWritten(long qtObject, int charHandle, byte[] newData,
1843 int errorCode);
1844 native void leDescriptorWritten(long qtObject, int charHandle, byte[] newData,
1845 int errorCode);
1846 native void leCharacteristicChanged(long qtObject, int charHandle, byte[] newData);
1847 native void leServiceError(long qtObject, int attributeHandle, int errorCode);
1848}
1849
quint8 rssi
id< QT_MANGLE_NAMESPACE(GCDTimerDelegate)> timeoutHandler
Definition btgcdtimer.mm:25
IOBluetoothDevice * device
std::vector< ObjCStrongReference< CBMutableService > > services
Definition lalr.h:268
void newState(QList< State > &states, const char *token, const char *lexem, bool pre)
Q_CORE_EXPORT QtJniTypes::Service service()
QTCONCURRENT_RUN_NODISCARD auto run(QThreadPool *pool, Function &&f, Args &&...args)
static const QCssKnownValue properties[NumProperties - 1]
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
GLuint64 GLenum void * handle
GLuint GLfloat GLfloat GLfloat x1
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum type
GLuint entry
GLuint64EXT * result
[6]
@ Handler
static QByteArray getDevice(const QString &rootPath)
QByteArray bytearray
[3]
QSettings settings("MySoft", "Star Runner")
[0]
QSharedPointer< T > other(t)
[5]
QNetworkAccessManager manager
QJSValueList args