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
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
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
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
650 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
651 super.onConnectionStateChange(gatt, status, newState);
652 handleOnConnectionStateChange(gatt, status, newState);
653 }
654
655 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
656 super.onServicesDiscovered(gatt, status);
657 handleOnServicesDiscovered(gatt, status);
658
659 }
660
661 // API < 33
662 public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
663 android.bluetooth.BluetoothGattCharacteristic characteristic,
664 int status)
665 {
666 super.onCharacteristicRead(gatt, characteristic, status);
667 handleOnCharacteristicRead(gatt, characteristic, characteristic.getValue(), status);
668 }
669
670 // API >= 33
671 public void onCharacteristicRead(android.bluetooth.BluetoothGatt gatt,
672 android.bluetooth.BluetoothGattCharacteristic characteristic,
673 byte[] value,
674 int status)
675 {
676 // Note: here we don't call the super implementation as it calls the old "< API 33"
677 // callback, and the callback would be handled twice
678 handleOnCharacteristicRead(gatt, characteristic, value, status);
679 }
680
681 public void onCharacteristicWrite(android.bluetooth.BluetoothGatt gatt,
682 android.bluetooth.BluetoothGattCharacteristic characteristic,
683 int status)
684 {
685 super.onCharacteristicWrite(gatt, characteristic, status);
686 handleOnCharacteristicWrite(gatt, characteristic, status);
687 }
688
689 // API < 33
690 public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
691 android.bluetooth.BluetoothGattCharacteristic characteristic)
692 {
693 super.onCharacteristicChanged(gatt, characteristic);
694 handleOnCharacteristicChanged(gatt, characteristic, characteristic.getValue());
695 }
696
697 // API >= 33
698 public void onCharacteristicChanged(android.bluetooth.BluetoothGatt gatt,
699 android.bluetooth.BluetoothGattCharacteristic characteristic,
700 byte[] value)
701 {
702 // Note: here we don't call the super implementation as it calls the old "< API 33"
703 // callback, and the callback would be handled twice
704 handleOnCharacteristicChanged(gatt, characteristic, value);
705 }
706
707 // API < 33
708 public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
709 android.bluetooth.BluetoothGattDescriptor descriptor,
710 int status)
711 {
712 super.onDescriptorRead(gatt, descriptor, status);
713 handleOnDescriptorRead(gatt, descriptor, status, descriptor.getValue());
714 }
715
716 // API >= 33
717 public void onDescriptorRead(android.bluetooth.BluetoothGatt gatt,
718 android.bluetooth.BluetoothGattDescriptor descriptor,
719 int status,
720 byte[] value)
721 {
722 // Note: here we don't call the super implementation as it calls the old "< API 33"
723 // callback, and the callback would be handled twice
724 handleOnDescriptorRead(gatt, descriptor, status, value);
725 }
726
727 public void onDescriptorWrite(android.bluetooth.BluetoothGatt gatt,
728 android.bluetooth.BluetoothGattDescriptor descriptor,
729 int status)
730 {
731 super.onDescriptorWrite(gatt, descriptor, status);
732 handleOnDescriptorWrite(gatt, descriptor, status);
733 }
734 //TODO currently not supported
735// public void onReliableWriteCompleted(android.bluetooth.BluetoothGatt gatt,
736// int status) {
737// System.out.println("onReliableWriteCompleted");
738// }
739//
740 public void onReadRemoteRssi(android.bluetooth.BluetoothGatt gatt, int rssi, int status)
741 {
742 super.onReadRemoteRssi(gatt, rssi, status);
743 handleOnReadRemoteRssi(gatt, rssi, status);
744 }
745
746 public void onMtuChanged(android.bluetooth.BluetoothGatt gatt, int mtu, int status)
747 {
748 super.onMtuChanged(gatt, mtu, status);
749 handleOnMtuChanged(gatt, mtu, status);
750 }
751 };
752
753 // This function is called from Qt thread
754 public synchronized int mtu() {
755 if (mSupportedMtu == -1) {
756 return DEFAULT_MTU;
757 } else {
758 return mSupportedMtu;
759 }
760 }
761
762 // This function is called from Qt thread
763 public synchronized boolean readRemoteRssi() {
764 if (mBluetoothGatt == null)
765 return false;
766
767 // Reading of RSSI can sometimes be 'lost' especially if amidst
768 // characteristic reads/writes ('lost' here meaning that there is no callback).
769 // To avoid this schedule the RSSI read in the job queue.
770 ReadWriteJob newJob = new ReadWriteJob();
771 newJob.jobType = IoJobType.Rssi;
772 newJob.entry = null;
773
774 if (!readWriteQueue.add(newJob)) {
775 Log.w(TAG, "Cannot add remote RSSI read to queue" );
776 return false;
777 }
778
779 performNextIOThreaded();
780 return true;
781 }
782
783 // This function is called from Qt thread
784 public synchronized boolean connect() {
785 BluetoothDevice mRemoteGattDevice;
786
787 if (mBluetoothAdapter == null) {
788 Log.w(TAG, "Cannot connect, no bluetooth adapter");
789 return false;
790 }
791
792 try {
793 mRemoteGattDevice = mBluetoothAdapter.getRemoteDevice(mRemoteGattAddress);
794 } catch (IllegalArgumentException ex) {
795 Log.w(TAG, "Remote address is not valid: " + mRemoteGattAddress);
796 return false;
797 }
798
799 /* The required connectGatt function is already available in SDK v26, but Android 8.0
800 * contains a race condition in the Changed callback such that it can return the value that
801 * was written. This is fixed in Android 8.1, which matches SDK v27. */
802 if (Build.VERSION.SDK_INT >= 27) {
803 HandlerThread handlerThread = new HandlerThread("QtBluetoothLEHandlerThread");
804 handlerThread.start();
805 mHandler = new Handler(handlerThread.getLooper());
806
807 Class[] args = new Class[6];
808 args[0] = android.content.Context.class;
809 args[1] = boolean.class;
810 args[2] = android.bluetooth.BluetoothGattCallback.class;
811 args[3] = int.class;
812 args[4] = int.class;
813 args[5] = android.os.Handler.class;
814
815 try {
816 Method connectMethod = mRemoteGattDevice.getClass().getDeclaredMethod("connectGatt", args);
817 if (connectMethod != null) {
818 mBluetoothGatt = (BluetoothGatt) connectMethod.invoke(mRemoteGattDevice, qtContext, false,
819 gattCallback, 2 /* TRANSPORT_LE */, 1 /*BluetoothDevice.PHY_LE_1M*/, mHandler);
820 Log.w(TAG, "Using Android v26 BluetoothDevice.connectGatt()");
821 }
822 } catch (Exception ex) {
823 Log.w(TAG, "connectGatt() v26 not available");
824 ex.printStackTrace();
825 }
826
827 if (mBluetoothGatt == null) {
828 mHandler.getLooper().quitSafely();
829 mHandler = null;
830 }
831 }
832
833 if (mBluetoothGatt == null) {
834 try {
835 //This API element is currently: greylist-max-o (API level 27), reflection, allowed
836 //It may change in the future
837 Class[] constr_args = new Class[5];
838 constr_args[0] = android.bluetooth.BluetoothGattService.class;
839 constr_args[1] = java.util.UUID.class;
840 constr_args[2] = int.class;
841 constr_args[3] = int.class;
842 constr_args[4] = int.class;
843 mCharacteristicConstructor = BluetoothGattCharacteristic.class.getDeclaredConstructor(constr_args);
844 mCharacteristicConstructor.setAccessible(true);
845 } catch (NoSuchMethodException ex) {
846 Log.w(TAG, "Unable get characteristic constructor. Buffer race condition are possible");
847 /* For some reason we don't get the private BluetoothGattCharacteristic ctor.
848 This means that we cannot protect ourselves from issues where concurrent
849 read and write operations on the same char can overwrite each others buffer.
850 Nevertheless we continue with best effort.
851 */
852 }
853 try {
854 mBluetoothGatt =
855 mRemoteGattDevice.connectGatt(qtContext, false,
856 gattCallback, 2 /* TRANSPORT_LE */);
857 } catch (IllegalArgumentException ex) {
858 Log.w(TAG, "Gatt connection failed");
859 ex.printStackTrace();
860 }
861 }
862 return mBluetoothGatt != null;
863 }
864
865 // This function is called from Qt thread
866 public synchronized void disconnect() {
867 if (mBluetoothGatt == null)
868 return;
869
870 mBluetoothGatt.disconnect();
871 }
872
873 // This function is called from Qt thread
874 public synchronized boolean discoverServices()
875 {
876 return mBluetoothGatt != null && mBluetoothGatt.discoverServices();
877 }
878
879 private enum GattEntryType
880 {
881 Service, Characteristic, CharacteristicValue, Descriptor
883 private class GattEntry
884 {
885 public GattEntryType type;
886 public boolean valueKnown = false;
887 public BluetoothGattService service = null;
888 public BluetoothGattCharacteristic characteristic = null;
889 public BluetoothGattDescriptor descriptor = null;
890 /*
891 * endHandle defined for GattEntryType.Service and GattEntryType.CharacteristicValue
892 * If the type is service this is the value of the last Gatt entry belonging to the very
893 * same service. If the type is a char value it is the entries index inside
894 * the "entries" list.
895 */
896 public int endHandle = -1;
897 // pointer back to the handle that describes the service that this GATT entry belongs to
898 public int associatedServiceHandle;
899 }
900
901 private enum IoJobType
902 {
903 Read, Write, Mtu,
905 // a skipped read is a read which is not executed
906 // introduced in Qt 6.2 to skip reads without changing service discovery logic
908
909 private class ReadWriteJob
910 {
911 public GattEntry entry;
912 public byte[] newValue;
913 public int requestedWriteType;
914 public IoJobType jobType;
915 }
916
917 // service uuid -> service handle mapping (there can be more than one service with same uuid)
918 private final Hashtable<UUID, List<Integer>> uuidToEntry = new Hashtable<UUID, List<Integer>>(100);
919 // index into array is equivalent to handle id
920 private final ArrayList<GattEntry> entries = new ArrayList<GattEntry>(100);
921 //backlog of to be discovered services
922 private final LinkedList<Integer> servicesToBeDiscovered = new LinkedList<Integer>();
923
924
925 private final LinkedList<ReadWriteJob> readWriteQueue = new LinkedList<ReadWriteJob>();
926 private ReadWriteJob pendingJob;
927
928 /*
929 Internal helper function
930 Returns the handle id for the given characteristic; otherwise returns -1.
931
932 Note that this is the Java handle. The Qt handle is the Java handle +1.
933 */
934 private int handleForCharacteristic(BluetoothGattCharacteristic characteristic)
935 {
936 if (characteristic == null)
937 return -1;
938
939 List<Integer> handles = uuidToEntry.get(characteristic.getService().getUuid());
940 if (handles == null || handles.isEmpty())
941 return -1;
942
943 //TODO for now we assume we always want the first service in case of uuid collision
944 int serviceHandle = handles.get(0);
945
946 try {
947 GattEntry entry;
948 for (int i = serviceHandle+1; i < entries.size(); i++) {
949 entry = entries.get(i);
950 if (entry == null)
951 continue;
952
953 switch (entry.type) {
954 case Descriptor:
955 case CharacteristicValue:
956 continue;
957 case Service:
958 break;
959 case Characteristic:
960 if (entry.characteristic == characteristic)
961 return i;
962 break;
963 }
964 }
965 } catch (IndexOutOfBoundsException ex) { /*nothing*/ }
966 return -1;
967 }
968
969 /*
970 Internal helper function
971 Returns the handle id for the given descriptor; otherwise returns -1.
972
973 Note that this is the Java handle. The Qt handle is the Java handle +1.
974 */
975 private int handleForDescriptor(BluetoothGattDescriptor descriptor)
976 {
977 if (descriptor == null)
978 return -1;
979
980 List<Integer> handles = uuidToEntry.get(descriptor.getCharacteristic().getService().getUuid());
981 if (handles == null || handles.isEmpty())
982 return -1;
983
984 //TODO for now we assume we always want the first service in case of uuid collision
985 int serviceHandle = handles.get(0);
986
987 try {
988 GattEntry entry;
989 for (int i = serviceHandle+1; i < entries.size(); i++) {
990 entry = entries.get(i);
991 if (entry == null)
992 continue;
993
994 switch (entry.type) {
995 case Characteristic:
996 case CharacteristicValue:
997 continue;
998 case Service:
999 break;
1000 case Descriptor:
1001 if (entry.descriptor == descriptor)
1002 return i;
1003 break;
1004 }
1005 }
1006 } catch (IndexOutOfBoundsException ignored) { }
1007 return -1;
1008 }
1009
1010 // This function is called from Qt thread (indirectly)
1011 private void populateHandles()
1012 {
1013 // We introduce the notion of artificial handles. While GATT handles
1014 // are not exposed on Android they help to quickly identify GATT attributes
1015 // on the C++ side. The Qt Api will not expose the handles
1016 GattEntry entry = null;
1017 List<BluetoothGattService> services = mBluetoothGatt.getServices();
1018 for (BluetoothGattService service: services) {
1019 GattEntry serviceEntry = new GattEntry();
1020 serviceEntry.type = GattEntryType.Service;
1021 serviceEntry.service = service;
1022 entries.add(serviceEntry);
1023
1024 // remember handle for the service for later update
1025 int serviceHandle = entries.size() - 1;
1026 //point to itself -> mostly done for consistence reasons with other entries
1027 serviceEntry.associatedServiceHandle = serviceHandle;
1028
1029 //some devices may have more than one service with the same uuid
1030 List<Integer> old = uuidToEntry.get(service.getUuid());
1031 if (old == null)
1032 old = new ArrayList<Integer>();
1033 old.add(entries.size()-1);
1034 uuidToEntry.put(service.getUuid(), old);
1035
1036 // add all characteristics
1037 List<BluetoothGattCharacteristic> charList = service.getCharacteristics();
1038 for (BluetoothGattCharacteristic characteristic: charList) {
1039 entry = new GattEntry();
1040 entry.type = GattEntryType.Characteristic;
1041 entry.characteristic = characteristic;
1042 entry.associatedServiceHandle = serviceHandle;
1043 //entry.endHandle = .. undefined
1044 entries.add(entry);
1045
1046 // this emulates GATT value attributes
1047 entry = new GattEntry();
1048 entry.type = GattEntryType.CharacteristicValue;
1049 entry.associatedServiceHandle = serviceHandle;
1050 entry.endHandle = entries.size(); // special case -> current index in entries list
1051 entries.add(entry);
1052
1053 // add all descriptors
1054 List<BluetoothGattDescriptor> descList = characteristic.getDescriptors();
1055 for (BluetoothGattDescriptor desc: descList) {
1056 entry = new GattEntry();
1057 entry.type = GattEntryType.Descriptor;
1058 entry.descriptor = desc;
1059 entry.associatedServiceHandle = serviceHandle;
1060 //entry.endHandle = .. undefined
1061 entries.add(entry);
1062 }
1063 }
1064
1065 // update endHandle of current service
1066 serviceEntry.endHandle = entries.size() - 1;
1067 }
1068
1069 entries.trimToSize();
1070 }
1071
1072 private void resetData()
1073 {
1074 uuidToEntry.clear();
1075 entries.clear();
1076 servicesToBeDiscovered.clear();
1077
1078 // kill all timeout handlers
1079 timeoutHandler.removeCallbacksAndMessages(null);
1080 handleForTimeout.set(HANDLE_FOR_RESET);
1081
1082 readWriteQueue.clear();
1083 pendingJob = null;
1084 }
1085
1086 // This function is called from Qt thread
1087 public synchronized boolean discoverServiceDetails(String serviceUuid, boolean fullDiscovery)
1088 {
1089 Log.d(TAG, "Discover service details for: " + serviceUuid + ", fullDiscovery: "
1090 + fullDiscovery + ", BluetoothGatt: " + (mBluetoothGatt != null));
1091 try {
1092 if (mBluetoothGatt == null)
1093 return false;
1094
1095 if (entries.isEmpty())
1096 populateHandles();
1097
1098 GattEntry entry;
1099 int serviceHandle;
1100 try {
1101 UUID service = UUID.fromString(serviceUuid);
1102 List<Integer> handles = uuidToEntry.get(service);
1103 if (handles == null || handles.isEmpty()) {
1104 Log.w(TAG, "Unknown service uuid for current device: " + service.toString());
1105 return false;
1106 }
1107
1108 //TODO for now we assume we always want the first service in case of uuid collision
1109 serviceHandle = handles.get(0);
1110 entry = entries.get(serviceHandle);
1111 if (entry == null) {
1112 Log.w(TAG, "Service with UUID " + service.toString() + " not found");
1113 return false;
1114 }
1115 } catch (IllegalArgumentException ex) {
1116 //invalid UUID string passed
1117 Log.w(TAG, "Cannot parse given UUID");
1118 return false;
1119 }
1120
1121 if (entry.type != GattEntryType.Service) {
1122 Log.w(TAG, "Given UUID is not a service UUID: " + serviceUuid);
1123 return false;
1124 }
1125
1126 // current service already discovered or under investigation
1127 if (entry.valueKnown || servicesToBeDiscovered.contains(serviceHandle)) {
1128 Log.w(TAG, "Service already known or to be discovered");
1129 return true;
1130 }
1131
1132 servicesToBeDiscovered.add(serviceHandle);
1133 scheduleServiceDetailDiscovery(serviceHandle, fullDiscovery);
1134 performNextIOThreaded();
1135 } catch (Exception ex) {
1136 ex.printStackTrace();
1137 return false;
1138 }
1139
1140 return true;
1141 }
1142
1143 /*
1144 Returns the uuids of the services included by the given service. Otherwise returns null.
1145 This function is called from Qt thread
1146 */
1147 public synchronized String includedServices(String serviceUuid)
1148 {
1149 if (mBluetoothGatt == null)
1150 return null;
1151
1152 UUID uuid;
1153 try {
1154 uuid = UUID.fromString(serviceUuid);
1155 } catch (Exception ex) {
1156 ex.printStackTrace();
1157 return null;
1158 }
1159
1160 //TODO Breaks in case of two services with same uuid
1161 BluetoothGattService service = mBluetoothGatt.getService(uuid);
1162 if (service == null)
1163 return null;
1164
1165 final List<BluetoothGattService> includes = service.getIncludedServices();
1166 if (includes.isEmpty())
1167 return null;
1168
1169 StringBuilder builder = new StringBuilder();
1170 for (BluetoothGattService includedService: includes) {
1171 builder.append(includedService.getUuid().toString()).append(" "); //space is separator
1172 }
1173
1174 return builder.toString();
1175 }
1176
1177 private synchronized void finishCurrentServiceDiscovery(int handleDiscoveredService)
1178 {
1179 Log.w(TAG, "Finished current discovery for service handle " + handleDiscoveredService);
1180 GattEntry discoveredService = entries.get(handleDiscoveredService);
1181 discoveredService.valueKnown = true;
1182 try {
1183 servicesToBeDiscovered.removeFirst();
1184 } catch (NoSuchElementException ex) {
1185 Log.w(TAG, "Expected queued service but didn't find any");
1186 }
1187
1188 leServiceDetailDiscoveryFinished(qtObject, discoveredService.service.getUuid().toString(),
1189 handleDiscoveredService + 1, discoveredService.endHandle + 1);
1190 }
1191
1192 // Executes under "this" client mutex. Returns true
1193 // if no actual MTU exchange is initiated
1194 private boolean executeMtuExchange()
1195 {
1196 if (mBluetoothGatt.requestMtu(MAX_MTU)) {
1197 Log.w(TAG, "MTU change initiated");
1198 return false;
1199 } else {
1200 Log.w(TAG, "MTU change request failed");
1201 }
1202
1203 Log.w(TAG, "Assuming default MTU value of 23 bytes");
1204 mSupportedMtu = DEFAULT_MTU;
1205 return true;
1206 }
1207
1208 private boolean executeRemoteRssiRead()
1209 {
1210 if (mBluetoothGatt.readRemoteRssi()) {
1211 Log.d(TAG, "RSSI read initiated");
1212 return false;
1213 }
1214 Log.w(TAG, "Initiating remote RSSI read failed");
1215 leRemoteRssiRead(qtObject, 0, false);
1216 return true;
1217 }
1218
1219 /*
1220 * Already executed in GattCallback so executed by the HandlerThread. No need to
1221 * post it to the Hander.
1222 */
1223 private void scheduleMtuExchange() {
1224 ReadWriteJob newJob = new ReadWriteJob();
1225 newJob.jobType = IoJobType.Mtu;
1226 newJob.entry = null;
1227
1228 readWriteQueue.add(newJob);
1229
1230 performNextIO();
1231 }
1232
1233 /*
1234 Internal Helper function for discoverServiceDetails()
1235
1236 Adds all Gatt entries for the given service to the readWriteQueue to be discovered.
1237 This function only ever adds read requests to the queue.
1238
1239 */
1240 private void scheduleServiceDetailDiscovery(int serviceHandle, boolean fullDiscovery)
1241 {
1242 GattEntry serviceEntry = entries.get(serviceHandle);
1243 final int endHandle = serviceEntry.endHandle;
1244
1245 if (serviceHandle == endHandle) {
1246 Log.w(TAG, "scheduleServiceDetailDiscovery: service is empty; nothing to discover");
1247 finishCurrentServiceDiscovery(serviceHandle);
1248 return;
1249 }
1250
1251 // serviceHandle + 1 -> ignore service handle itself
1252 for (int i = serviceHandle + 1; i <= endHandle; i++) {
1253 GattEntry entry = entries.get(i);
1254
1255 if (entry.type == GattEntryType.Service) {
1256 // should not really happen unless endHandle is wrong
1257 Log.w(TAG, "scheduleServiceDetailDiscovery: wrong endHandle");
1258 return;
1259 }
1260
1261 ReadWriteJob newJob = new ReadWriteJob();
1262 newJob.entry = entry;
1263 if (fullDiscovery) {
1264 newJob.jobType = IoJobType.Read;
1265 } else {
1266 newJob.jobType = IoJobType.SkippedRead;
1267 }
1268
1269 final boolean result = readWriteQueue.add(newJob);
1270 if (!result)
1271 Log.w(TAG, "Cannot add service discovery job for " + serviceEntry.service.getUuid()
1272 + " on item " + entry.type);
1273 }
1274 }
1275
1276 /*************************************************************/
1277 /* Write Characteristics */
1278 /* This function is called from Qt thread */
1279 /*************************************************************/
1280
1281 public synchronized boolean writeCharacteristic(int charHandle, byte[] newValue,
1282 int writeMode)
1283 {
1284 if (mBluetoothGatt == null)
1285 return false;
1286
1287 GattEntry entry;
1288 try {
1289 entry = entries.get(charHandle-1); //Qt always uses handles+1
1290 } catch (IndexOutOfBoundsException ex) {
1291 ex.printStackTrace();
1292 return false;
1293 }
1294
1295 ReadWriteJob newJob = new ReadWriteJob();
1296 newJob.newValue = newValue;
1297 newJob.entry = entry;
1298 newJob.jobType = IoJobType.Write;
1299
1300 // writeMode must be in sync with QLowEnergyService::WriteMode
1301 switch (writeMode) {
1302 case 1: //WriteWithoutResponse
1303 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
1304 break;
1305 case 2: //WriteSigned
1306 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_SIGNED;
1307 break;
1308 default:
1309 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
1310 break;
1311 }
1312
1313 boolean result;
1314 result = readWriteQueue.add(newJob);
1315
1316 if (!result) {
1317 Log.w(TAG, "Cannot add characteristic write request for " + charHandle + " to queue" );
1318 return false;
1319 }
1320
1321 performNextIOThreaded();
1322 return true;
1323 }
1324
1325 /*************************************************************/
1326 /* Write Descriptors */
1327 /* This function is called from Qt thread */
1328 /*************************************************************/
1329
1330 public synchronized boolean writeDescriptor(int descHandle, byte[] newValue)
1331 {
1332 if (mBluetoothGatt == null)
1333 return false;
1334
1335 GattEntry entry;
1336 try {
1337 entry = entries.get(descHandle-1); //Qt always uses handles+1
1338 } catch (IndexOutOfBoundsException ex) {
1339 ex.printStackTrace();
1340 return false;
1341 }
1342
1343 ReadWriteJob newJob = new ReadWriteJob();
1344 newJob.newValue = newValue;
1345 newJob.entry = entry;
1346 newJob.requestedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
1347 newJob.jobType = IoJobType.Write;
1348
1349 boolean result;
1350 result = readWriteQueue.add(newJob);
1351
1352 if (!result) {
1353 Log.w(TAG, "Cannot add descriptor write request for " + descHandle + " to queue" );
1354 return false;
1355 }
1356
1357 performNextIOThreaded();
1358 return true;
1359 }
1360
1361 /*************************************************************/
1362 /* Read Characteristics */
1363 /* This function is called from Qt thread */
1364 /*************************************************************/
1365
1366 public synchronized boolean readCharacteristic(int charHandle)
1367 {
1368 if (mBluetoothGatt == null)
1369 return false;
1370
1371 GattEntry entry;
1372 try {
1373 entry = entries.get(charHandle-1); //Qt always uses handles+1
1374 } catch (IndexOutOfBoundsException ex) {
1375 ex.printStackTrace();
1376 return false;
1377 }
1378
1379 ReadWriteJob newJob = new ReadWriteJob();
1380 newJob.entry = entry;
1381 newJob.jobType = IoJobType.Read;
1382
1383 boolean result;
1384 result = readWriteQueue.add(newJob);
1385
1386 if (!result) {
1387 Log.w(TAG, "Cannot add characteristic read request for " + charHandle + " to queue" );
1388 return false;
1389 }
1390
1391 performNextIOThreaded();
1392 return true;
1393 }
1394
1395 // This function is called from Qt thread
1396 public synchronized boolean readDescriptor(int descHandle)
1397 {
1398 if (mBluetoothGatt == null)
1399 return false;
1400
1401 GattEntry entry;
1402 try {
1403 entry = entries.get(descHandle-1); //Qt always uses handles+1
1404 } catch (IndexOutOfBoundsException ex) {
1405 ex.printStackTrace();
1406 return false;
1407 }
1408
1409 ReadWriteJob newJob = new ReadWriteJob();
1410 newJob.entry = entry;
1411 newJob.jobType = IoJobType.Read;
1412
1413 boolean result;
1414 result = readWriteQueue.add(newJob);
1415
1416 if (!result) {
1417 Log.w(TAG, "Cannot add descriptor read request for " + descHandle + " to queue" );
1418 return false;
1419 }
1420
1421 performNextIOThreaded();
1422 return true;
1423 }
1424
1425 // Called by TimeoutRunnable if the current I/O job timed out.
1426 // By the time we reach this point the handleForTimeout counter has already been reset
1427 // and the regular responses will be blocked off.
1428 private synchronized void interruptCurrentIO(int handle)
1429 {
1430 //unlock the queue for next item
1431 pendingJob = null;
1432
1433 performNextIOThreaded();
1434
1435 if (handle == HANDLE_FOR_MTU_EXCHANGE || handle == HANDLE_FOR_RSSI_READ)
1436 return;
1437
1438 try {
1439 GattEntry entry = entries.get(handle);
1440 if (entry == null)
1441 return;
1442 if (entry.valueKnown)
1443 return;
1444 entry.valueKnown = true;
1445
1446 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
1447 if (serviceEntry != null && serviceEntry.endHandle == handle)
1448 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
1449 } catch (IndexOutOfBoundsException outOfBounds) {
1450 Log.w(TAG, "interruptCurrentIO(): Unknown gatt entry, index: "
1451 + handle + " size: " + entries.size());
1452 }
1453 }
1454
1455 /*
1456 Wrapper around performNextIO() ensuring that performNextIO() is executed inside
1457 the mHandler/mHandlerThread if it exists.
1458 */
1459 private void performNextIOThreaded()
1460 {
1461 if (mHandler != null) {
1462 mHandler.post(new Runnable() {
1463 @Override
1464 public void run() {
1465 performNextIO();
1466 }
1467 });
1468 } else {
1469 performNextIO();
1470 }
1471 }
1472
1473 /*
1474 The queuing is required because two writeCharacteristic/writeDescriptor calls
1475 cannot execute at the same time. The second write must happen after the
1476 previous write has finished with on(Characteristic|Descriptor)Write().
1477 */
1478 private synchronized void performNextIO()
1479 {
1480 Log.d(TAG, "Perform next BTLE IO, job queue size: " + readWriteQueue.size()
1481 + ", a job is pending: " + (pendingJob != null) + ", BluetoothGatt: "
1482 + (mBluetoothGatt != null));
1483
1484 if (mBluetoothGatt == null)
1485 return;
1486
1487 boolean skip = false;
1488 final ReadWriteJob nextJob;
1489 int handle = HANDLE_FOR_RESET;
1490
1491 if (readWriteQueue.isEmpty() || pendingJob != null)
1492 return;
1493
1494 nextJob = readWriteQueue.remove();
1495 // MTU requests and RSSI reads are special cases
1496 if (nextJob.jobType == IoJobType.Mtu) {
1497 handle = HANDLE_FOR_MTU_EXCHANGE;
1498 } else if (nextJob.jobType == IoJobType.Rssi) {
1499 handle = HANDLE_FOR_RSSI_READ;
1500 } else {
1501 switch (nextJob.entry.type) {
1502 case Characteristic:
1503 handle = handleForCharacteristic(nextJob.entry.characteristic);
1504 break;
1505 case Descriptor:
1506 handle = handleForDescriptor(nextJob.entry.descriptor);
1507 break;
1508 case CharacteristicValue:
1509 handle = nextJob.entry.endHandle;
1510 default:
1511 break;
1512 }
1513 }
1514
1515 // timeout handler and handleForTimeout atomic must be setup before
1516 // executing the request. Sometimes the callback is quicker than executing the
1517 // remainder of this function. Therefore enable the atomic early
1518 timeoutHandler.removeCallbacksAndMessages(null); // remove any timeout handlers
1519 handleForTimeout.set(modifiedReadWriteHandle(handle, nextJob.jobType));
1520
1521 switch (nextJob.jobType) {
1522 case Read:
1523 skip = executeReadJob(nextJob);
1524 break;
1525 case SkippedRead:
1526 skip = true;
1527 break;
1528 case Write:
1529 skip = executeWriteJob(nextJob);
1530 break;
1531 case Mtu:
1532 skip = executeMtuExchange();
1533 case Rssi:
1534 skip = executeRemoteRssiRead();
1535 break;
1536 }
1537
1538 if (skip) {
1539 handleForTimeout.set(HANDLE_FOR_RESET); // not a pending call -> release atomic
1540 } else {
1541 pendingJob = nextJob;
1542 timeoutHandler.postDelayed(new TimeoutRunnable(
1543 modifiedReadWriteHandle(handle, nextJob.jobType)), RUNNABLE_TIMEOUT);
1544 }
1545
1546 if (nextJob.jobType != IoJobType.Mtu && nextJob.jobType != IoJobType.Rssi) {
1547 Log.d(TAG, "Performing queued job, handle: " + handle + " " + nextJob.jobType + " (" +
1548 (nextJob.requestedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) +
1549 ") ValueKnown: " + nextJob.entry.valueKnown + " Skipping: " + skip +
1550 " " + nextJob.entry.type);
1551 }
1552
1553 GattEntry entry = nextJob.entry;
1554
1555 if (skip) {
1556 /*
1557 BluetoothGatt.[read|write][Characteristic|Descriptor]() immediately
1558 return in cases where meta data doesn't match the intended action
1559 (e.g. trying to write to read-only char). When this happens
1560 we have to report an error back to Qt. The error report is not required during
1561 the initial service discovery though.
1562 */
1563 if (handle > HANDLE_FOR_RESET) {
1564 // during service discovery we do not report error but emit characteristicRead()
1565 // any other time a failure emits serviceError() signal
1566
1567 final boolean isServiceDiscovery = !entry.valueKnown;
1568
1569 if (isServiceDiscovery) {
1570 entry.valueKnown = true;
1571 switch (entry.type) {
1572 case Characteristic:
1573 Log.d(TAG,
1574 nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
1575 + " characteristic " + entry.characteristic.getUuid()
1576 + " for service " + entry.characteristic.getService().getUuid());
1577 leCharacteristicRead(qtObject, entry.characteristic.getService().getUuid().toString(),
1578 handle + 1, entry.characteristic.getUuid().toString(),
1579 entry.characteristic.getProperties(), null);
1580 break;
1581 case Descriptor:
1582 Log.d(TAG,
1583 nextJob.jobType == IoJobType.Read ? "Non-readable" : "Skipped reading of"
1584 + " descriptor " + entry.descriptor.getUuid()
1585 + " for service/char " + entry.descriptor.getCharacteristic().getService().getUuid()
1586 + "/" + entry.descriptor.getCharacteristic().getUuid());
1587 leDescriptorRead(qtObject,
1588 entry.descriptor.getCharacteristic().getService().getUuid().toString(),
1589 entry.descriptor.getCharacteristic().getUuid().toString(),
1590 handle + 1, entry.descriptor.getUuid().toString(),
1591 null);
1592 break;
1593 case CharacteristicValue:
1594 // for more details see scheduleServiceDetailDiscovery(int, boolean)
1595 break;
1596 case Service:
1597 Log.w(TAG, "Scheduling of Service Gatt entry for service discovery should never happen.");
1598 break;
1599 }
1600
1601 // last entry of current discovery run?
1602 try {
1603 GattEntry serviceEntry = entries.get(entry.associatedServiceHandle);
1604 if (serviceEntry.endHandle == handle)
1605 finishCurrentServiceDiscovery(entry.associatedServiceHandle);
1606 } catch (IndexOutOfBoundsException outOfBounds) {
1607 Log.w(TAG, "performNextIO(): Unknown service for entry, index: "
1608 + entry.associatedServiceHandle + " size: " + entries.size());
1609 }
1610 } else {
1611 int errorCode = 0;
1612
1613 // The error codes below must be in sync with QLowEnergyService::ServiceError
1614 if (nextJob.jobType == IoJobType.Read) {
1615 errorCode = (entry.type == GattEntryType.Characteristic) ?
1616 5 : 6; // CharacteristicReadError : DescriptorReadError
1617 } else {
1618 errorCode = (entry.type == GattEntryType.Characteristic) ?
1619 2 : 3; // CharacteristicWriteError : DescriptorWriteError
1620 }
1621
1622 leServiceError(qtObject, handle + 1, errorCode);
1623 }
1624 }
1625
1626 performNextIO();
1627 }
1628 }
1629
1630 private BluetoothGattCharacteristic cloneChararacteristic(BluetoothGattCharacteristic other) {
1631 try {
1632 return (BluetoothGattCharacteristic) mCharacteristicConstructor.newInstance(other.getService(),
1633 other.getUuid(), other.getInstanceId(), other.getProperties(), other.getPermissions());
1634 } catch (Exception ex) {
1635 Log.w(TAG, "Cloning characteristic failed!" + ex);
1636 return null;
1637 }
1638 }
1639
1640 // Returns true if nextJob should be skipped.
1641 private boolean executeWriteJob(ReadWriteJob nextJob)
1642 {
1643 boolean result;
1644 switch (nextJob.entry.type) {
1645 case Characteristic:
1646 if (Build.VERSION.SDK_INT >= 33) {
1647 int writeResult = mBluetoothGatt.writeCharacteristic(
1648 nextJob.entry.characteristic, nextJob.newValue, nextJob.requestedWriteType);
1649 return (writeResult != BluetoothStatusCodes.SUCCESS);
1650 }
1651 if (mHandler != null || mCharacteristicConstructor == null) {
1652 if (nextJob.entry.characteristic.getWriteType() != nextJob.requestedWriteType) {
1653 nextJob.entry.characteristic.setWriteType(nextJob.requestedWriteType);
1654 }
1655 result = nextJob.entry.characteristic.setValue(nextJob.newValue);
1656 return !result || !mBluetoothGatt.writeCharacteristic(nextJob.entry.characteristic);
1657 } else {
1658 BluetoothGattCharacteristic orig = nextJob.entry.characteristic;
1659 BluetoothGattCharacteristic tmp = cloneChararacteristic(orig);
1660 if (tmp == null)
1661 return true;
1662 tmp.setWriteType(nextJob.requestedWriteType);
1663 return !tmp.setValue(nextJob.newValue) || !mBluetoothGatt.writeCharacteristic(tmp);
1664 }
1665 case Descriptor:
1666 if (nextJob.entry.descriptor.getUuid().compareTo(clientCharacteristicUuid) == 0) {
1667 /*
1668 For some reason, Android splits characteristic notifications
1669 into two operations. BluetoothGatt.enableCharacteristicNotification
1670 ensures the local Bluetooth stack forwards the notifications. In addition,
1671 BluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
1672 must be written to the peripheral.
1673 */
1674
1675
1676 /* There is no documentation on indication behavior. The assumption is
1677 that when indication or notification are requested we call
1678 BluetoothGatt.setCharacteristicNotification. Furthermore it is assumed
1679 indications are send via onCharacteristicChanged too and Android itself
1680 will do the confirmation required for an indication as per
1681 Bluetooth spec Vol 3, Part G, 4.11 . If neither of the two bits are set
1682 we disable the signals.
1683 */
1684 boolean enableNotifications = false;
1685 int value = (nextJob.newValue[0] & 0xff);
1686 // first or second bit must be set
1687 if (((value & 0x1) == 1) || (((value >> 1) & 0x1) == 1)) {
1688 enableNotifications = true;
1689 }
1690
1691 result = mBluetoothGatt.setCharacteristicNotification(
1692 nextJob.entry.descriptor.getCharacteristic(), enableNotifications);
1693 if (!result) {
1694 Log.w(TAG, "Cannot set characteristic notification");
1695 //we continue anyway to ensure that we write the requested value
1696 //to the device
1697 }
1698
1699 Log.d(TAG, "Enable notifications: " + enableNotifications);
1700 }
1701
1702 if (Build.VERSION.SDK_INT >= 33) {
1703 int writeResult = mBluetoothGatt.writeDescriptor(
1704 nextJob.entry.descriptor, nextJob.newValue);
1705 return (writeResult != BluetoothStatusCodes.SUCCESS);
1706 }
1707 result = nextJob.entry.descriptor.setValue(nextJob.newValue);
1708 if (!result || !mBluetoothGatt.writeDescriptor(nextJob.entry.descriptor))
1709 return true;
1710
1711 break;
1712 case Service:
1713 case CharacteristicValue:
1714 return true;
1715 }
1716 return false;
1717 }
1718
1719 // Returns true if nextJob should be skipped.
1720 private boolean executeReadJob(ReadWriteJob nextJob)
1721 {
1722 boolean result;
1723 switch (nextJob.entry.type) {
1724 case Characteristic:
1725 try {
1726 result = mBluetoothGatt.readCharacteristic(nextJob.entry.characteristic);
1727 } catch (java.lang.SecurityException se) {
1728 // QTBUG-59917 -> HID services cause problems since Android 5.1
1729 se.printStackTrace();
1730 result = false;
1731 }
1732 if (!result)
1733 return true; // skip
1734 break;
1735 case Descriptor:
1736 try {
1737 result = mBluetoothGatt.readDescriptor(nextJob.entry.descriptor);
1738 } catch (java.lang.SecurityException se) {
1739 // QTBUG-59917 -> HID services cause problems since Android 5.1
1740 se.printStackTrace();
1741 result = false;
1742 }
1743 if (!result)
1744 return true; // skip
1745 break;
1746 case Service:
1747 return true;
1748 case CharacteristicValue:
1749 return true; //skip
1750 }
1751 return false;
1752 }
1753
1754 /*
1755 * Modifies and returns the given \a handle such that the job
1756 * \a type is encoded into the returned handle. Hereby we take advantage of the fact that
1757 * a Bluetooth Low Energy handle is only 16 bit. The handle will be the bottom two bytes
1758 * and the job type will be in the top 2 bytes.
1759 *
1760 * top 2 bytes
1761 * - 0x01 -> Read Job
1762 * - 0x02 -> Write Job
1763 *
1764 * This is done in connection with handleForTimeout and assists in the process of
1765 * detecting accidental interruption by the timeout handler.
1766 * If two requests for the same handle are scheduled behind each other there is the
1767 * theoretical chance that the first request comes back normally while the second request
1768 * is interrupted by the timeout handler. This risk still exists but this function ensures that
1769 * at least back to back requests of differing types cannot affect each other via the timeout
1770 * handler.
1771 */
1772 private int modifiedReadWriteHandle(int handle, IoJobType type)
1773 {
1774 int modifiedHandle = handle;
1775 // ensure we have 16bit handle only
1776 if (handle > 0xFFFF)
1777 Log.w(TAG, "Invalid handle");
1778
1779 modifiedHandle = (modifiedHandle & 0xFFFF);
1780
1781 switch (type) {
1782 case Write:
1783 modifiedHandle = (modifiedHandle | 0x00010000);
1784 break;
1785 case Read:
1786 modifiedHandle = (modifiedHandle | 0x00020000);
1787 break;
1788 case Mtu:
1789 modifiedHandle = HANDLE_FOR_MTU_EXCHANGE;
1790 break;
1791 case Rssi:
1792 modifiedHandle = HANDLE_FOR_RSSI_READ;
1793 break;
1794 }
1795
1796 return modifiedHandle;
1797 }
1798
1799 // This function is called from Qt thread
1800 public synchronized boolean requestConnectionUpdatePriority(double minimalInterval)
1801 {
1802 if (mBluetoothGatt == null)
1803 return false;
1804
1805 int requestPriority = 0; // BluetoothGatt.CONNECTION_PRIORITY_BALANCED
1806 if (minimalInterval < 30)
1807 requestPriority = 1; // BluetoothGatt.CONNECTION_PRIORITY_HIGH
1808 else if (minimalInterval > 100)
1809 requestPriority = 2; //BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER
1810
1811 try {
1812 return mBluetoothGatt.requestConnectionPriority(requestPriority);
1813 } catch (IllegalArgumentException ex) {
1814 Log.w(TAG, "Connection update priority out of range: " + requestPriority);
1815 return false;
1816 }
1817 }
1818
1819 public native void leConnectionStateChange(long qtObject, int wasErrorTransition, int newState);
1820 public native void leMtuChanged(long qtObject, int mtu);
1821 public native void leRemoteRssiRead(long qtObject, int rssi, boolean success);
1822 public native void leServicesDiscovered(long qtObject, int errorCode, String uuidList);
1823 public native void leServiceDetailDiscoveryFinished(long qtObject, final String serviceUuid,
1824 int startHandle, int endHandle);
1825 public native void leCharacteristicRead(long qtObject, String serviceUuid,
1826 int charHandle, String charUuid,
1827 int properties, byte[] data);
1828 public native void leDescriptorRead(long qtObject, String serviceUuid, String charUuid,
1829 int descHandle, String descUuid, byte[] data);
1830 public native void leCharacteristicWritten(long qtObject, int charHandle, byte[] newData,
1831 int errorCode);
1832 public native void leDescriptorWritten(long qtObject, int charHandle, byte[] newData,
1833 int errorCode);
1834 public native void leCharacteristicChanged(long qtObject, int charHandle, byte[] newData);
1835 public native void leServiceError(long qtObject, int attributeHandle, int errorCode);
1836}
1837
quint8 rssi
id< QT_MANGLE_NAMESPACE(GCDTimerDelegate)> timeoutHandler
Definition btgcdtimer.mm:25
IOBluetoothDevice * device
std::vector< ObjCStrongReference< CBMutableService > > services
Definition lalr.h:268
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)
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
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