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