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