-
Notifications
You must be signed in to change notification settings - Fork 76
/
BlePeripheral.java
1123 lines (958 loc) · 49 KB
/
BlePeripheral.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package com.adafruit.bluefruit.le.connect.ble.central;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_SCAN;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.adafruit.bluefruit.le.connect.BuildConfig;
import com.adafruit.bluefruit.le.connect.ble.BleUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import no.nordicsemi.android.support.v18.scanner.ScanRecord;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
@SuppressWarnings({"ConstantConditions", "PointlessBooleanExpression", "WeakerAccess", "unused"})
public class BlePeripheral {
// Log
private final static String TAG = BlePeripheral.class.getSimpleName();
// Config
private final static boolean kSetPreferredMtuSize = true;
private static final int kPreferredMtuSize = 517;
private final static boolean kSetPhy_2M = true;
// Constants
public static final int STATE_DISCONNECTED = BluetoothProfile.STATE_DISCONNECTED;
public static final int STATE_CONNECTING = BluetoothProfile.STATE_CONNECTING;
public static final int STATE_CONNECTED = BluetoothProfile.STATE_CONNECTED;
public static final int STATE_DISCONNECTING = BluetoothProfile.STATE_DISCONNECTING;
// Config
private final static boolean kDebugCommands = BuildConfig.DEBUG && true; // Set a identifier for each command and verifies that the command processed is the one expected
private final static boolean kProfileTimeouts = BuildConfig.DEBUG && true;
private final static String kPrefix = "com.adafruit.bluefruit.bleperipheral.";
private static final int kDefaultMtuSize = 20;
public final static String kBlePeripheral_OnConnecting = kPrefix + "connecting";
public final static String kBlePeripheral_OnConnected = kPrefix + "connected";
public final static String kBlePeripheral_OnDisconnected = kPrefix + "disconnected";
public final static String kBlePeripheral_OnReconnecting = kPrefix + "reconnecting";
public final static String kBlePeripheral_OnRssiUpdated = kPrefix + "rssiUpdated";
public final static String kExtra_deviceAddress = kPrefix + "extra_deviceAddress";
public final static String kExtra_expectedDisconnect = kPrefix + "extra_expectedDisconnect";
public final static UUID kClientCharacteristicConfigUUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private final static boolean kForceWriteWithoutResponse = false;//true; // Force without response, or take into account that write response (onCharacteristicWrite) could be reported AFTER onCharacteristicChanged on expecting a response
private static final boolean kHackToAvoidProblemsWhenWriteIsReceivedBeforeChangedOnWriteWithResponse = true; // On Android when writing on a characteristic with writetype WRITE_TYPE_DEFAULT, onCharacteristicChanged (when a response is expected) can be called before onCharacteristicWrite. This weird behaviour has to be taken into account!!
// Data
private ScanResult mScanResult;
private LocalBroadcastManager mLocalBroadcastManager;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private final CommandQueue mCommandQueue = new CommandQueue();
private final Map<String, NotifyHandler> mNotifyHandlers = new HashMap<>();
private final List<CaptureReadHandler> mCaptureReadHandlers = new ArrayList<>();
private int mRssi = 0;
private int mMtuSize = kDefaultMtuSize;
private boolean cachedNameNeedsUpdate = true; // false if the cached name has been recovered (cachedName could be null, so testing for null is not enough)
private String cachedName = null; // Cached name
private String cachedAddress = null; // Cached address
private Context mConnectionContext = null;
private boolean isAutoreconnectOnDisconnectionEnabled = false;
// region BluetoothGattCallback
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@SuppressLint("InlinedApi")
@RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_CONNECT})
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
Log.d(TAG, "onConnectionStateChange from: " + status + " to:" + newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
mConnectionState = STATE_CONNECTED;
isAutoreconnectOnDisconnectionEnabled = true;
// Phy
if (kSetPhy_2M) {
Log.d(TAG, "Set Phy to 2M");
setPreferredPhy(BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED);
}
// MTU
if (kSetPreferredMtuSize) {
// Increase MTU packet size
requestMtu(kPreferredMtuSize, null); // Note: requestMtu only affects to WriteWithoutResponse
}
localBroadcastUpdate(kBlePeripheral_OnConnected, getIdentifier());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "onConnectionStateChange STATE_DISCONNECTED");
notifyConnectionFinished();
} else {
Log.w(TAG, "unknown onConnectionStateChange from: " + status + " to:" + newState);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
finishExecutingCommand(status);
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.d(TAG, "onCharacteristicRead");
if (kDebugCommands) {
final String identifier = getCharacteristicIdentifier(characteristic);
BleCommand command = mCommandQueue.first();
if (command.mType == BleCommand.BLECOMMANDTYPE_READCHARACTERISTIC && identifier.equals(command.mIdentifier)) {
finishExecutingCommand(status);
} else {
Log.w(TAG, "Warning: onCharacteristicRead with no matching command");
}
} else {
finishExecutingCommand(status);
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
BleCommand command = mCommandQueue.first();
if (command != null && !command.mIsCancelled && command.mType == BleCommand.BLECOMMANDTYPE_WRITECHARACTERISTICANDWAITNOTIFY) {
if (kHackToAvoidProblemsWhenWriteIsReceivedBeforeChangedOnWriteWithResponse) {
Log.d(TAG, "onCharacteristicWrite. Ignored");
// TODO: fixit
// This is not totally correct. If onCharacteristicChanged arrives before onCharacteristicWrite, onCharacteristicChanged should not finishExecutingCommand and wait should be executed when this function is called
} else {
Log.d(TAG, "onCharacteristicWrite. Waiting for response");
final String identifier = getCharacteristicIdentifier(characteristic);
if (kDebugCommands && !identifier.equals(command.mIdentifier)) {
Log.w(TAG, "Warning: onCharacteristicWrite with no matching command");
}
BleCommandCaptureReadParameters readParameters = (BleCommandCaptureReadParameters) command.mExtra;
CaptureReadHandler captureReadHandler = new CaptureReadHandler(readParameters.readIdentifier, readParameters.completionHandler, readParameters.timeout, mTimeoutRemoveCaptureHandler);
Log.d(TAG, "onCharacteristicWrite: add captureReadHandler");
mCaptureReadHandlers.add(captureReadHandler);
}
}
/*
else if (command != null && command.mType == BleCommand.BLECOMMANDTYPE_WRITECHARACTERISTIC && characteristic.getWriteType() == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
// Note on Android bluetooth stack: https://stackoverflow.com/questions/43741849/oncharacteristicwrite-and-onnotificationsent-are-being-called-too-fast-how-to/43744888
// TODO: use this instead of simulating a response for WRITE_TYPE_NO_RESPONSE
Log.w(TAG, "onCharacteristicWrite received for WRITE_TYPE_NO_RESPONSE");
}*/
else {
Log.d(TAG, "onCharacteristicWrite. Finished");
finishExecutingCommand(status);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.d(TAG, "onCharacteristicChanged. numCaptureReadHandlers: " + mCaptureReadHandlers.size());
final String identifier = getCharacteristicIdentifier(characteristic);
final int status = BluetoothGatt.GATT_SUCCESS; // On Android, there is no error reported for this callback, so we assume it is SUCCESS
// Check if waiting to capture this read
boolean isNotifyOmitted = false;
boolean hasCaptureHandler = false;
// Remove capture handler
final int captureHandlerIndex = getCaptureHandlerIndex(identifier);
if (captureHandlerIndex >= 0) {
hasCaptureHandler = true;
CaptureReadHandler captureReadHandler = mCaptureReadHandlers.remove(captureHandlerIndex);
// Cancel timeout handler
if (captureReadHandler.mTimeoutTimer != null) {
if (kProfileTimeouts) {
Log.d(TAG, "Cancel timeout: " + captureReadHandler.mIdentifier + ". elapsed millis:" + (System.currentTimeMillis() - captureReadHandler.mTimeoutStartingMillis));
}
captureReadHandler.mTimeoutTimer.cancel();
captureReadHandler.mTimeoutTimer = null;
}
// Send result
byte[] value = characteristic.getValue();
Log.d(TAG, "onCharacteristicChanged: send result to captureReadHandler:" + BleUtils.bytesToHex2(value));
captureReadHandler.mResult.read(status, value);
isNotifyOmitted = captureReadHandler.mIsNotifyOmitted;
}
// Notify
if (!isNotifyOmitted) {
NotifyHandler notifyHandler = mNotifyHandlers.get(identifier);
if (notifyHandler != null) {
notifyHandler.notify(status);
}
}
if (hasCaptureHandler) {
finishExecutingCommand(status);
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
if (kDebugCommands) {
final String identifier = getDescriptorIdentifier(descriptor.getCharacteristic().getService().getUuid(), descriptor.getCharacteristic().getUuid(), descriptor.getUuid());
BleCommand command = mCommandQueue.first();
if (command.mType == BleCommand.BLECOMMANDTYPE_READDESCRIPTOR && identifier.equals(command.mIdentifier)) {
finishExecutingCommand(status);
} else {
Log.w(TAG, "Warning: onDescriptorRead with no matching command");
}
} else {
finishExecutingCommand(status);
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
//final String identifier = getDescriptorIdentifier(descriptor.getCharacteristic().getService().getUuid(), descriptor.getCharacteristic().getUuid(), descriptor.getUuid());
BleCommand command = mCommandQueue.first();
if (command != null && command.mType == BleCommand.BLECOMMANDTYPE_SETNOTIFY) {
if (kDebugCommands) {
final String identifier = getCharacteristicIdentifier(descriptor.getCharacteristic());
if (identifier.equals(command.mIdentifier)) {
//Log.d(TAG, "Set Notify descriptor write: " + status);
finishExecutingCommand(status);
} else {
Log.w(TAG, "Warning: onDescriptorWrite for BLECOMMANDTYPE_SETNOTIFY with no matching command");
}
} else {
finishExecutingCommand(status);
}
} else {
Log.w(TAG, "Warning: onDescriptorWrite with no matching command");
}
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
super.onReliableWriteCompleted(gatt, status);
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
mRssi = rssi;
localBroadcastUpdate(kBlePeripheral_OnRssiUpdated, getIdentifier());
} else {
Log.w(TAG, "onReadRemoteRssi error: " + status);
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
mMtuSize = mtu;
Log.d(TAG, "Mtu changed: " + mtu);
} else {
Log.d(TAG, "Error changing mtu to: " + mtu + " status: " + status);
}
// Check that the MTU changed callback was called in response to a command
BleCommand command = mCommandQueue.first();
if (command.mType == BleCommand.BLECOMMANDTYPE_REQUESTMTU) {
finishExecutingCommand(status);
}
}
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
super.onPhyUpdate(gatt, txPhy, rxPhy, status);
Log.d(TAG, "onPhyUpdate -> tx: " + txPhy + " rx: " + rxPhy + " status: " + status);
}
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
super.onPhyRead(gatt, txPhy, rxPhy, status);
Log.d(TAG, "onPhyRead -> tx: " + txPhy + " rx: " + rxPhy + " status: " + status);
}
};
//
BlePeripheral(ScanResult scanResult) {
replaceScanResult(scanResult);
}
// Properties
public int getMtuSize() {
return mMtuSize;
}
int getMaxPacketLength() {
return getMtuSize() - 3; // 3 bytes are used for internal purposes so the maximum size is MTU-3
}
public int getLastRssi() {
return mRssi;
}
private static int getCharacteristicProperties(BluetoothGattService service, UUID characteristicUUID) {
BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
int properties = 0;
if (characteristic != null) {
properties = characteristic.getProperties();
}
return properties;
}
@SuppressWarnings("UnnecessaryLocalVariable")
public static boolean isCharacteristicReadable(BluetoothGattService service, UUID characteristicUUID) {
final int properties = getCharacteristicProperties(service, characteristicUUID);
final boolean isReadable = (properties & BluetoothGattCharacteristic.PROPERTY_READ) != 0;
return isReadable;
}
@SuppressWarnings("UnnecessaryLocalVariable")
public static boolean isCharacteristicNotifiable(BluetoothGattService service, UUID characteristicUUID) {
final int properties = getCharacteristicProperties(service, characteristicUUID);
final boolean isNotifiable = (properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0;
return isNotifiable;
}
// Permissions
private static int getDescriptorPermissions(BluetoothGattService service, String characteristicUUIDString, String descriptorUUIDString) {
final UUID characteristicUuid = UUID.fromString(characteristicUUIDString);
BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUuid);
int permissions = 0;
if (characteristic != null) {
final UUID descriptorUuid = UUID.fromString(descriptorUUIDString);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(descriptorUuid);
if (descriptor != null) {
permissions = descriptor.getPermissions();
}
}
return permissions;
}
@SuppressWarnings("UnnecessaryLocalVariable")
public static boolean isDescriptorReadable(BluetoothGattService service, String characteristicUUIDString, String descriptorUUIDString) {
final int permissions = getDescriptorPermissions(service, characteristicUUIDString, descriptorUUIDString);
final boolean isReadable = (permissions & BluetoothGattCharacteristic.PERMISSION_READ) != 0;
return isReadable;
}
public static boolean isCharacteristicNotifyingForCachedClientConfigDescriptor(@NonNull BluetoothGattCharacteristic characteristic) {
// Note: client characteristic descriptor should have been read previously
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(kClientCharacteristicConfigUUID);
if (descriptor != null) {
byte[] configValue = descriptor.getValue();
return Arrays.equals(configValue, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
} else {
return false;
}
}
void replaceScanResult(ScanResult scanResult) {
mScanResult = scanResult;
}
public int getConnectionState() {
return mConnectionState;
}
public String getIdentifier() {
if (cachedAddress == null) {
cachedAddress = mScanResult.getDevice().getAddress();
}
return cachedAddress;
}
public String getName() {
if (cachedNameNeedsUpdate) {
String name = null;
try {
name = mScanResult.getDevice().getName();
} catch (SecurityException e) {
Log.w(TAG, "getName security exception: " + e);
}
if (name == null) {
name = getScanRecord().getDeviceName();
}
cachedNameNeedsUpdate = false;
cachedName = name;
}
return cachedName;
}
public ScanRecord getScanRecord() {
return mScanResult.getScanRecord();
}
public int getRssi() {
return mScanResult.getRssi();
}
public void reset() {
mRssi = 0;
mNotifyHandlers.clear();
mCaptureReadHandlers.clear();
BleCommand firstCommand = mCommandQueue.first();
if (firstCommand != null) {
firstCommand.cancel(); // Stop current command if is processing
}
mCommandQueue.clear();
}
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
public @NonNull
BluetoothDevice getDevice() {
return mScanResult.getDevice();
}
@SuppressLint("InlinedApi")
@RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_CONNECT})
@MainThread
public void connect(Context context) {
mConnectionContext = context;
mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
BluetoothDevice device = mScanResult.getDevice();
mCommandQueue.clear();
mConnectionState = STATE_CONNECTING;
localBroadcastUpdate(kBlePeripheral_OnConnecting, getIdentifier());
BleManager.getInstance().cancelDiscovery(); // Always cancel discovery before connecting
mBluetoothGatt = device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
if (mBluetoothGatt == null) {
Log.e(TAG, "connectGatt Error. Returns null");
}
}
@SuppressLint("InlinedApi")
@RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_CONNECT})
@MainThread
public void disconnect() {
if (mBluetoothGatt != null) {
final boolean wasConnecting = mConnectionState == STATE_CONNECTING;
mConnectionState = STATE_DISCONNECTING; // Important: set to disconnecting to signal that the disconnection was expected (check notifyConnectionFinished code)
mBluetoothGatt.disconnect();
if (wasConnecting) { // Force a disconnect broadcast because it will not be generated by the OS
notifyConnectionFinished();
}
}
}
public boolean isDisconnected() {
return mConnectionState == STATE_DISCONNECTED;
}
@SuppressLint("InlinedApi")
@RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_CONNECT})
private void notifyConnectionFinished() {
final boolean isExpected = mConnectionState == STATE_DISCONNECTING;
mConnectionState = STATE_DISCONNECTED;
if (isExpected) {
localBroadcastUpdate(kBlePeripheral_OnDisconnected, getIdentifier(), kExtra_expectedDisconnect, kExtra_expectedDisconnect); // Send a extra parameter (kExtra_expectedDisconnect) with any value, so it is known that was expected (and no message errors are displayed to the user)
} else {
if (isAutoreconnectOnDisconnectionEnabled) {
Log.d(TAG, "Trying to reconnect to peripheral: " + getName());
localBroadcastUpdate(kBlePeripheral_OnReconnecting, getIdentifier());
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> connect(mConnectionContext));
} else {
localBroadcastUpdate(kBlePeripheral_OnDisconnected, getIdentifier());
}
isAutoreconnectOnDisconnectionEnabled = false;
}
closeBluetoothGatt();
//mLocalBroadcastManager = null;
mConnectionContext = null;
}
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
public boolean readRssi() {
// if true: Caller should wait for onReadRssi callback. False when rssi read is not available
return mBluetoothGatt != null && mBluetoothGatt.readRemoteRssi();
}
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) {
if (mBluetoothGatt != null) {
Log.d(TAG, "setPreferredPhy");
mBluetoothGatt.setPreferredPhy(txPhy, rxPhy, phyOptions);
//mBluetoothGatt.readPhy();
}
}
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
public void readPhy() {
if (mBluetoothGatt != null) {
mBluetoothGatt.readPhy();
}
}
// endregion
// region CommandQueue
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@MainThread
private void closeBluetoothGatt() {
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mCommandQueue.clear();
mBluetoothGatt = null;
}
}
private void localBroadcastUpdate(@NonNull final String action, @NonNull String deviceAddress) {
localBroadcastUpdate(action, deviceAddress, null, null);
}
private void localBroadcastUpdate(@NonNull final String action, @NonNull String deviceAddress, @Nullable String extraParamKey, @Nullable String extraParamValue) {
if (mLocalBroadcastManager != null) {
final Intent intent = new Intent(action);
intent.putExtra(kExtra_deviceAddress, deviceAddress);
if (extraParamKey != null) {
intent.putExtra(extraParamKey, extraParamValue);
}
mLocalBroadcastManager.sendBroadcast(intent);
}
else {
Log.w(TAG, "localBroadcastUpdate with invalid manager");
}
}
private void finishExecutingCommand(int status) {
BleCommand command = mCommandQueue.first();
if (command != null && !command.mIsCancelled) {
command.completion(status);
}
mCommandQueue.executeNext();
}
public void discoverServices(CompletionHandler completionHandler) {
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_DISCOVERSERVICES, null, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
final boolean isDiscoveryInProgress = mBluetoothGatt != null && mBluetoothGatt.discoverServices();
if (!isDiscoveryInProgress) {
Log.w(TAG, "Warning: discoverServices failed");
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
}
};
mCommandQueue.add(command);
}
public boolean isDiscoveringServices() {
return mCommandQueue.containsCommandType(BleCommand.BLECOMMANDTYPE_DISCOVERSERVICES);
}
// endregion
// region Commands
private String getCharacteristicIdentifier(@NonNull BluetoothGattCharacteristic characteristic) {
return getCharacteristicIdentifier(characteristic.getService().getUuid(), characteristic.getUuid());
}
private String getCharacteristicIdentifier(@NonNull UUID serviceUUID, @NonNull UUID characteristicUUID) {
return serviceUUID + characteristicUUID.toString();
}
private String getDescriptorIdentifier(@NonNull UUID serviceUUID, @NonNull UUID characteristicUUID, @NonNull UUID descriptorUUID) {
return serviceUUID + characteristicUUID.toString() + descriptorUUID;
}
public @Nullable
List<BluetoothGattService> getServices() {
// This function requires that service discovery has been completed for the given device or returns null
return mBluetoothGatt == null ? null : mBluetoothGatt.getServices();
}
public @Nullable
BluetoothGattService getService(@NonNull UUID uuid) {
// This function requires that service discovery has been completed for the given device.
// If multiple instance of the service exist, it returns the first one
return mBluetoothGatt == null ? null : mBluetoothGatt.getService(uuid);
}
public @Nullable
BluetoothGattCharacteristic getCharacteristic(@NonNull UUID characteristicUUID, @NonNull UUID serviceUUID) {
// This function requires that service discovery has been completed for the given device.
BluetoothGattService service = getService(serviceUUID);
return service == null ? null : service.getCharacteristic(characteristicUUID);
}
public void characteristicEnableNotify(@NonNull final BluetoothGattCharacteristic characteristic, NotifyHandler notifyHandler, CompletionHandler completionHandler) {
final String identifier = getCharacteristicIdentifier(characteristic);
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_SETNOTIFY, kDebugCommands ? identifier : null, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(kClientCharacteristicConfigUUID);
if (mBluetoothGatt != null && descriptor != null && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
mNotifyHandlers.put(identifier, notifyHandler);
mBluetoothGatt.setCharacteristicNotification(characteristic, true);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
//characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
} else {
Log.w(TAG, "enable notify: client config descriptor not found for characteristic: " + characteristic.getUuid().toString());
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
}
};
mCommandQueue.add(command);
}
public void characteristicDisableNotify(@NonNull final BluetoothGattCharacteristic characteristic, CompletionHandler completionHandler) {
final String identifier = getCharacteristicIdentifier(characteristic);
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_SETNOTIFY, kDebugCommands ? identifier : null, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(kClientCharacteristicConfigUUID);
if (mBluetoothGatt != null && descriptor != null && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
mNotifyHandlers.remove(identifier);
descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
} else {
Log.w(TAG, "disable notify: client config descriptor not found for characteristic: " + characteristic.getUuid().toString());
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
}
};
mCommandQueue.add(command);
}
public void characteristicUpdateNotify(@NonNull final BluetoothGattCharacteristic characteristic, NotifyHandler notifyHandler) {
final String identifier = getCharacteristicIdentifier(characteristic);
NotifyHandler previousNotifyHandler = mNotifyHandlers.put(identifier, notifyHandler);
if (previousNotifyHandler == null) {
Log.d(TAG, "trying to update inexistent notifyHandler for characteristic: " + characteristic.getUuid().toString());
}
}
public void readCharacteristic(@NonNull BluetoothGattService service, UUID characteristicUUID, @Nullable DataReadHandler dataReadHandler) {
final BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
if (characteristic != null) {
readCharacteristic(characteristic, dataReadHandler);
} else {
dataReadHandler.completion(BluetoothGatt.GATT_FAILURE, null);
}
}
public void readCharacteristic(@NonNull BluetoothGattCharacteristic characteristic, @Nullable DataReadHandler dataReadHandler) {
readCharacteristic(characteristic, status -> {
byte[] data = null;
if (status == BluetoothGatt.GATT_SUCCESS) {
data = characteristic.getValue();
}
dataReadHandler.completion(status, data);
});
}
public void readCharacteristic(@NonNull BluetoothGattService service, UUID characteristicUUID, @Nullable CompletionHandler completionHandler) {
final BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
if (characteristic != null) {
readCharacteristic(characteristic, completionHandler);
} else {
completionHandler.completion(BluetoothGatt.GATT_FAILURE);
}
}
public void readCharacteristic(@NonNull BluetoothGattCharacteristic characteristic, @Nullable CompletionHandler completionHandler) {
final String identifier = kDebugCommands ? getCharacteristicIdentifier(characteristic.getService().getUuid(), characteristic.getUuid()) : null;
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_READCHARACTERISTIC, identifier, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
if (mBluetoothGatt != null) {
// Read Characteristic
if (!kDebugCommands || (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
mBluetoothGatt.readCharacteristic(characteristic);
} else {
Log.w(TAG, "read: characteristic not readable: " + characteristic.getUuid().toString());
finishExecutingCommand(BluetoothGatt.GATT_READ_NOT_PERMITTED);
}
} else {
Log.w(TAG, "read: characteristic not found: " + characteristic.getUuid().toString());
finishExecutingCommand(BluetoothGatt.GATT_READ_NOT_PERMITTED);
}
}
};
mCommandQueue.add(command);
}
public void writeCharacteristic(@NonNull BluetoothGattCharacteristic characteristic, int writeType, @NonNull byte[] data, @Nullable CompletionHandler completionHandler) {
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_WRITECHARACTERISTIC, kDebugCommands ? getCharacteristicIdentifier(characteristic) : null, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
if (mBluetoothGatt != null) {
// Write value
int selectedWriteType;
if (kForceWriteWithoutResponse) {
selectedWriteType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
} else {
selectedWriteType = writeType;
}
characteristic.setWriteType(selectedWriteType);
characteristic.setValue(data);
final boolean success = mBluetoothGatt.writeCharacteristic(characteristic);
if (success) {
// Simulate response if needed
// Android: no need to simulate response: https://stackoverflow.com/questions/43741849/oncharacteristicwrite-and-onnotificationsent-are-being-called-too-fast-how-to/43744888
/*
if (selectedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
finishExecutingCommand(BluetoothGatt.GATT_SUCCESS);
}*/
} else {
Log.w(TAG, "writeCharacteristic could not be initiated");
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
} else {
Log.w(TAG, "mBluetoothGatt is null");
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
}
};
mCommandQueue.add(command);
}
public void writeCharacteristicAndCaptureNotify(@NonNull BluetoothGattCharacteristic characteristic, int writeType, @NonNull byte[] data, @Nullable CompletionHandler completionHandler, @NonNull BluetoothGattCharacteristic readCharacteristic, int readTimeout, @Nullable CaptureReadCompletionHandler readCompletionHandler) {
final String readIdentifier = getCharacteristicIdentifier(readCharacteristic);
BleCommandCaptureReadParameters captureReadParameters = new BleCommandCaptureReadParameters(readIdentifier, readCompletionHandler, readTimeout);
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_WRITECHARACTERISTICANDWAITNOTIFY, getCharacteristicIdentifier(characteristic), completionHandler, captureReadParameters) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
if (mBluetoothGatt != null) {
Log.d(TAG, "writeCharacteristicAndCaptureNotify");
// Write value
characteristic.setWriteType(writeType);
characteristic.setValue(data);
final boolean success = mBluetoothGatt.writeCharacteristic(characteristic);
if (success) {
// Simulate response if needed
if (characteristic.getWriteType() == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
if (!isCancelled()) {
CaptureReadHandler captureReadHandler = new CaptureReadHandler(readIdentifier, readCompletionHandler, readTimeout, mTimeoutRemoveCaptureHandler);
mCaptureReadHandlers.add(captureReadHandler);
}
finishExecutingCommand(BluetoothGatt.GATT_SUCCESS);
} else if (kHackToAvoidProblemsWhenWriteIsReceivedBeforeChangedOnWriteWithResponse) {
if (!isCancelled()) {
CaptureReadHandler captureReadHandler = new CaptureReadHandler(readIdentifier, readCompletionHandler, readTimeout, mTimeoutRemoveCaptureHandler);
mCaptureReadHandlers.add(captureReadHandler);
}
finishExecutingCommand(BluetoothGatt.GATT_SUCCESS);
}
} else {
Log.w(TAG, "writeCharacteristic could not be initiated");
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
} else {
Log.w(TAG, "mBluetoothGatt is null");
finishExecutingCommand(BluetoothGatt.GATT_FAILURE);
}
}
};
mCommandQueue.add(command);
}
public void readDescriptor(@NonNull BluetoothGattService service, UUID characteristicUUID, UUID descriptorUUID, CompletionHandler completionHandler) {
final BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
if (characteristic != null) {
readDescriptor(characteristic, descriptorUUID, completionHandler);
} else {
Log.w(TAG, "read: characteristic not found: " + characteristicUUID.toString());
finishExecutingCommand(BluetoothGatt.GATT_READ_NOT_PERMITTED);
}
}
public void readDescriptor(@NonNull BluetoothGattCharacteristic characteristic, UUID descriptorUUID, CompletionHandler completionHandler) {
final String identifier = kDebugCommands ? getDescriptorIdentifier(characteristic.getService().getUuid(), characteristic.getUuid(), descriptorUUID) : null;
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_READDESCRIPTOR, identifier, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
// Read Descriptor
final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(descriptorUUID);
if (descriptor != null) {
mBluetoothGatt.readDescriptor(descriptor);
} else {
Log.w(TAG, "read: descriptor not found: " + descriptorUUID.toString());
finishExecutingCommand(BluetoothGatt.GATT_READ_NOT_PERMITTED);
}
}
};
mCommandQueue.add(command);
}
public void requestMtu(@IntRange(from = 23, to = 517) int mtuSize, CompletionHandler completionHandler) {
final String identifier = null;
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_REQUESTMTU, identifier, completionHandler) {
@SuppressLint("InlinedApi")
@RequiresPermission(value = BLUETOOTH_CONNECT)
@Override
public void execute() {
// Request mtu size change
Log.d(TAG, "Request mtu change");
mBluetoothGatt.requestMtu(mtuSize);
}
};
mCommandQueue.add(command);
}
/*
@RequiresApi(api = Build.VERSION_CODES.O)
public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions, CompletionHandler completionHandler) {
final String identifier = null;
BleCommand command = new BleCommand(BleCommand.BLECOMMANDTYPE_SETPREFERREDPHY, identifier, completionHandler) {
@Override
public void execute() {
Log.d(TAG, "Set preferred Phy");
mBluetoothGatt.setPreferredPhy(txPhy, rxPhy, phyOptions);
}
};
mCommmandQueue.add(command);
}*/
public interface CompletionHandler {
void completion(int status);
}
public interface ProgressHandler {
void progress(float progress);
}
public interface NotifyHandler {
void notify(int status);
}
public interface DataReadHandler {
void completion(int status, @Nullable byte[] data);
}
public interface CaptureReadCompletionHandler {
void read(int status, @Nullable byte[] value);
interface TimeoutAction {
void execute(String identifier);
}
}
// endregion
// region CaptureReadHandler
public static final int kPeripheralReadTimeoutError = -1; // Value should be different that errors defined in BluetoothGatt.GATT_*
static class CaptureReadHandler {
private final String mIdentifier;
private final CaptureReadCompletionHandler mResult;
private Timer mTimeoutTimer;
private long mTimeoutStartingMillis; // only used for debug (kProfileTimeouts)
private CaptureReadCompletionHandler.TimeoutAction mTimeoutAction;
private final boolean mIsNotifyOmitted;
CaptureReadHandler(String identifier, CaptureReadCompletionHandler result, int timeout, @Nullable CaptureReadCompletionHandler.TimeoutAction timeoutAction) {
this(identifier, result, timeout, timeoutAction, false);
}
CaptureReadHandler(String identifier, CaptureReadCompletionHandler result, int timeout, @Nullable CaptureReadCompletionHandler.TimeoutAction timeoutAction, boolean isNotifyOmitted) {
mIdentifier = identifier;
mResult = result;
mIsNotifyOmitted = isNotifyOmitted;
// Setup timeout if not zero
if (timeout > 0 && timeoutAction != null) {
mTimeoutAction = timeoutAction;
mTimeoutTimer = new Timer();
if (kProfileTimeouts) {
mTimeoutStartingMillis = System.currentTimeMillis();
Log.d(TAG, "Start timeout: " + identifier + ". millis:" + mTimeoutStartingMillis);
}
mTimeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
if (kProfileTimeouts) {
Log.d(TAG, "Fire timeout: " + identifier + ". elapsed millis:" + (System.currentTimeMillis() - mTimeoutStartingMillis));
}
mResult.read(kPeripheralReadTimeoutError, null);
mTimeoutAction.execute(identifier);
}
}, timeout);
}
}
}
private int getCaptureHandlerIndex(String identifier) {
boolean found = false;
int i = 0;
if (mCaptureReadHandlers.size() > 0) {
while (i < mCaptureReadHandlers.size() && !found) {
if (mCaptureReadHandlers.get(i).mIdentifier.equals(identifier)) {
found = true;
} else {
i++;
}
}
}
return found ? i : -1;
}
private final CaptureReadCompletionHandler.TimeoutAction mTimeoutRemoveCaptureHandler = identifier -> { // Default behaviour for a capture handler timeout
// Remove capture handler
final int captureHandlerIndex = getCaptureHandlerIndex(identifier);
if (captureHandlerIndex >= 0) {
mCaptureReadHandlers.remove(captureHandlerIndex);
}
finishExecutingCommand(kPeripheralReadTimeoutError);
};
static class BleCommandCaptureReadParameters {
final String readIdentifier;
final CaptureReadCompletionHandler completionHandler;