From 4821b40e35102b243c313855cc26908efab929ab Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Tue, 9 Aug 2022 22:27:19 +1000 Subject: [PATCH 01/11] Improve Android 11 background permissions request ordering --- src/android/BLECentralPlugin.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/android/BLECentralPlugin.java b/src/android/BLECentralPlugin.java index 7dee77b2..4a520a39 100644 --- a/src/android/BLECentralPlugin.java +++ b/src/android/BLECentralPlugin.java @@ -1081,6 +1081,19 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic if (!PermissionHelper.hasPermission(this, BLUETOOTH_CONNECT)) { missingPermissions.add(BLUETOOTH_CONNECT); } + } else if (COMPILE_SDK_VERSION >= 30 && Build.VERSION.SDK_INT >= 30) { // (API 30) Build.VERSION_CODES.R + // Android 11 specifically requires FINE location access to be granted first before + // the app is allowed to ask for ACCESS_BACKGROUND_LOCATION + // Source: https://developer.android.com/about/versions/11/privacy/location + if (!PermissionHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) { + missingPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); + } else { + String accessBackgroundLocation = this.preferences.getString("accessBackgroundLocation", "false"); + if (accessBackgroundLocation == "true" && !PermissionHelper.hasPermission(this, ACCESS_BACKGROUND_LOCATION)) { + LOG.w(TAG, "ACCESS_BACKGROUND_LOCATION is being requested"); + missingPermissions.add(ACCESS_BACKGROUND_LOCATION); + } + } } else if (COMPILE_SDK_VERSION >= 29 && Build.VERSION.SDK_INT >= 29) { // (API 29) Build.VERSION_CODES.Q if (!PermissionHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) { missingPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); From ba0219f3f3a3e1446079398ba95243098a643d3a Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Thu, 11 Aug 2022 20:21:20 +1000 Subject: [PATCH 02/11] Document advanced android scan options --- types.d.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/types.d.ts b/types.d.ts index 46c603e7..adb48021 100644 --- a/types.d.ts +++ b/types.d.ts @@ -5,7 +5,7 @@ declare namespace BLECentralPlugin { service: string; characteristic: string; properties: string[]; - descriptors?: any[] | undefined; + descriptors?: any[]; } interface PeripheralData { @@ -28,7 +28,22 @@ declare namespace BLECentralPlugin { } interface StartScanOptions { - reportDuplicates?: boolean | undefined; + /* Android only */ + scanMode?: 'lowPower' | 'balanced' | 'lowLatency' | 'opportunistic'; + /* Android only */ + callbackType?: 'all' | 'first' | 'lost'; + /* Android only */ + matchMode?: 'aggressive' | 'sticky'; + /* Android only */ + numOfMatches?: 'one' | 'few' | 'max'; + /* Android only */ + phy?: '1m' | 'coded' | 'all'; + /* Android only */ + legacy?: boolean; + /* Android only */ + reportDelay?: number; + + reportDuplicates?: boolean; } interface L2CAPOptions { @@ -223,7 +238,7 @@ declare namespace BLECentralPlugin { service_uuid: string, characteristic_uuid: string, success: (rawData: ArrayBuffer | 'registered') => any, - failure?: (error: string | BLEError) => any, + failure: (error: string | BLEError) => any, options: { emitOnRegistered: boolean } ): void; From 09e05cf70fa235aa324f7cfa6d5ac4c69302d01e Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Fri, 14 Oct 2022 23:43:31 +1100 Subject: [PATCH 03/11] Prevent NPE if device disconnects before device cache refresh #936 Fixes #936 --- src/android/Peripheral.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/android/Peripheral.java b/src/android/Peripheral.java index 6ce85a1b..de18277a 100644 --- a/src/android/Peripheral.java +++ b/src/android/Peripheral.java @@ -229,11 +229,17 @@ public void refreshDeviceCache(CallbackContext callback, final long timeoutMilli if (success) { this.refreshCallback = callback; Handler handler = new Handler(); + LOG.d(TAG, "Waiting " + timeoutMillis + " milliseconds before discovering services"); handler.postDelayed(new Runnable() { @Override public void run() { - LOG.d(TAG, "Waiting " + timeoutMillis + " milliseconds before discovering services"); - gatt.discoverServices(); + if (gatt != null) { + try { + gatt.discoverServices(); + } catch(Exception e) { + LOG.e(TAG, "refreshDeviceCache Failed after delay", e); + } + } } }, timeoutMillis); } From 0f8a2f7182d9eaec8d429341bc3fe7e33bf850d1 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Fri, 14 Oct 2022 23:48:50 +1100 Subject: [PATCH 04/11] Prevent NPE after L2CAP socket disconnects --- src/android/L2CAPContext.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/android/L2CAPContext.java b/src/android/L2CAPContext.java index 1568be91..f81df043 100644 --- a/src/android/L2CAPContext.java +++ b/src/android/L2CAPContext.java @@ -110,9 +110,10 @@ public void writeL2CapChannel(CallbackContext callbackContext, byte[] data) { @RequiresApi(api = Build.VERSION_CODES.M) private void readL2CapData() { try { - InputStream inputStream = socket.getInputStream(); - byte[] buffer = new byte[socket.getMaxReceivePacketSize()]; - while (socket.isConnected()) { + final BluetoothSocket lSocket = this.socket; + InputStream inputStream = lSocket.getInputStream(); + byte[] buffer = new byte[lSocket.getMaxReceivePacketSize()]; + while (lSocket.isConnected()) { int readCount = inputStream.read(buffer); CallbackContext receiver; synchronized (updateLock) { From 6abdc1114733d6ecc4de13a2d3fbfa8827201d39 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Sat, 15 Oct 2022 00:05:13 +1100 Subject: [PATCH 05/11] Improve release documentation --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 85d0c1f0..5d91087a 100644 --- a/README.md +++ b/README.md @@ -1322,6 +1322,21 @@ Run the app on your phone cordova run android --device +# Release process + +## Pre-release + +1. `npm version prepatch --preid=alpha` +2. Align `plugin.xml` version with npm version +3. `npm publish --tag alpha --registry=https://registry.npmjs.org` + +## Release + +1. `npm version patch` +2. Align `plugin.xml` version with npm version +3. Update release notes +4. `npm publish --registry=https://registry.npmjs.org` + # Nordic DFU If you need Nordic DFU capability, Tomáš Bedřich has a [fork](https://github.com/fxe-gear/cordova-plugin-ble-central) of this plugin that adds an `updateFirmware()` method that allows users to upgrade nRF5x based chips over the air. https://github.com/fxe-gear/cordova-plugin-ble-central From 26a142dcc9a4330591cf2b90a32987bc897b8db8 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Sat, 15 Oct 2022 00:46:28 +1100 Subject: [PATCH 06/11] Android: Prevent various exceptions on stopScan #901 #871 Fixes #901 and #871 --- src/android/BLECentralPlugin.java | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/android/BLECentralPlugin.java b/src/android/BLECentralPlugin.java index 4a520a39..0feef265 100644 --- a/src/android/BLECentralPlugin.java +++ b/src/android/BLECentralPlugin.java @@ -117,7 +117,6 @@ public class BLECentralPlugin extends CordovaPlugin { private static final int REQUEST_ENABLE_BLUETOOTH = 1; BluetoothAdapter bluetoothAdapter; - BluetoothLeScanner bluetoothLeScanner; // key is the MAC Address Map peripherals = new LinkedHashMap(); @@ -193,7 +192,6 @@ public boolean execute(String action, CordovaArgs args, CallbackContext callback } BluetoothManager bluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter(); - bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); } boolean validAction = true; @@ -212,8 +210,7 @@ public boolean execute(String action, CordovaArgs args, CallbackContext callback findLowEnergyDevices(callbackContext, serviceUUIDs, -1); } else if (action.equals(STOP_SCAN)) { - - bluetoothLeScanner.stopScan(leScanCallback); + stopScan(); callbackContext.success(); } else if (action.equals(LIST)) { @@ -1142,7 +1139,7 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic } discoverCallback = callbackContext; - bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); + final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); List filters = new ArrayList(); if (serviceUUIDs != null && serviceUUIDs.length > 0) { for (UUID uuid : serviceUUIDs) { @@ -1158,8 +1155,7 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic handler.postDelayed(new Runnable() { @Override public void run() { - LOG.d(TAG, "Stopping Scan"); - bluetoothLeScanner.stopScan(leScanCallback); + stopScan(); } }, scanSeconds * 1000); } @@ -1169,6 +1165,19 @@ public void run() { callbackContext.sendPluginResult(result); } + private void stopScan() { + if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { + LOG.d(TAG, "Stopping Scan"); + try { + final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); + if (bluetoothLeScanner != null) + bluetoothLeScanner.stopScan(leScanCallback); + } catch (Exception e) { + LOG.e(TAG, "Exception stopping scan", e); + } + } + } + private boolean locationServicesEnabled() { int locationMode = 0; try { From bd2734b85982862cbf0980f94ed3cffaac311523 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Sat, 15 Oct 2022 00:30:24 +1100 Subject: [PATCH 07/11] Android: Clear up time-based stopScan when new scan is started #902 Fixes #902 --- src/android/BLECentralPlugin.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/android/BLECentralPlugin.java b/src/android/BLECentralPlugin.java index 0feef265..b219c7c0 100644 --- a/src/android/BLECentralPlugin.java +++ b/src/android/BLECentralPlugin.java @@ -35,6 +35,7 @@ import android.content.pm.PackageManager; import android.content.IntentFilter; import android.os.Handler; +import android.os.Looper; import android.os.Build; import android.provider.Settings; @@ -135,6 +136,7 @@ public class BLECentralPlugin extends CordovaPlugin { private UUID[] serviceUUIDs; private int scanSeconds; private ScanSettings scanSettings; + private final Handler stopScanHandler = new Handler(Looper.getMainLooper()); // Bluetooth state notification CallbackContext stateCallback; @@ -1148,16 +1150,11 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic filters.add(filter); } } + stopScanHandler.removeCallbacks(this::stopScan); bluetoothLeScanner.startScan(filters, scanSettings, leScanCallback); if (scanSeconds > 0) { - Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - stopScan(); - } - }, scanSeconds * 1000); + stopScanHandler.postDelayed(this::stopScan, scanSeconds * 1000); } PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); @@ -1166,6 +1163,7 @@ public void run() { } private void stopScan() { + stopScanHandler.removeCallbacks(this::stopScan); if (bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { LOG.d(TAG, "Stopping Scan"); try { From 4c2ceab242db08b71a609c1303d9c35e873d7a00 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Sat, 15 Oct 2022 00:55:26 +1100 Subject: [PATCH 08/11] Android: Report if Bluetooth is disabled when scanning/connecting #826 Fixes #826 --- src/android/BLECentralPlugin.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/android/BLECentralPlugin.java b/src/android/BLECentralPlugin.java index b219c7c0..0dc4e5dc 100644 --- a/src/android/BLECentralPlugin.java +++ b/src/android/BLECentralPlugin.java @@ -709,6 +709,12 @@ private void connect(CallbackContext callbackContext, String macAddress) { } } + if (bluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { + LOG.w(TAG, "Tried to connect while Bluetooth is disabled."); + callbackContext.error("Bluetooth is disabled."); + return; + } + if (!peripherals.containsKey(macAddress) && BLECentralPlugin.this.bluetoothAdapter.checkBluetoothAddress(macAddress)) { BluetoothDevice device = BLECentralPlugin.this.bluetoothAdapter.getRemoteDevice(macAddress); Peripheral peripheral = new Peripheral(device); @@ -737,6 +743,12 @@ private void autoConnect(CallbackContext callbackContext, String macAddress) { } } + if (bluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { + LOG.w(TAG, "Tried to connect while Bluetooth is disabled."); + callbackContext.error("Bluetooth is disabled."); + return; + } + Peripheral peripheral = peripherals.get(macAddress); // allow auto-connect to connect to devices without scanning @@ -1120,6 +1132,12 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic } + if (bluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { + LOG.w(TAG, "Tried to start scan while Bluetooth is disabled."); + callbackContext.error("Bluetooth is disabled."); + return; + } + // return error if already scanning if (bluetoothAdapter.isDiscovering()) { LOG.w(TAG, "Tried to start scan while already running."); From 8d574ca5eb6a5e86765dda9c0a17d6c7aa1f9a1c Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Sat, 15 Oct 2022 01:11:12 +1100 Subject: [PATCH 09/11] Android: More carefully manage permission callbacks to avoid NPEs #773 #698 Should fix #773 and #698 --- src/android/BLECentralPlugin.java | 47 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/android/BLECentralPlugin.java b/src/android/BLECentralPlugin.java index 0dc4e5dc..36003635 100644 --- a/src/android/BLECentralPlugin.java +++ b/src/android/BLECentralPlugin.java @@ -1250,6 +1250,20 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { /* @Override */ public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) { + final CallbackContext callback = this.popPermissionsCallback(); + if (callback == null) { + if (grantResults.length > 0) { + // There are some odd happenings if permission requests are made while booting up capacitor + LOG.w(TAG, "onRequestPermissionResult received with no pending callback"); + } + return; + } + + if (grantResults.length == 0) { + callback.error("No permissions not granted."); + return; + } + //Android 12 (API 31) and higher // Users MUST accept BLUETOOTH_SCAN and BLUETOOTH_CONNECT // Android 10 (API 29) up to Android 11 (API 30) @@ -1258,22 +1272,21 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int // Android 9 (API 28) and lower // Users MUST accept ACCESS_COARSE_LOCATION for (int i = 0; i < permissions.length; i++) { - if (permissions[i].equals(Manifest.permission.ACCESS_FINE_LOCATION) && grantResults[i] == PackageManager.PERMISSION_DENIED) { LOG.d(TAG, "User *rejected* Fine Location Access"); - this.permissionCallback.error("Location permission not granted."); + callback.error("Location permission not granted."); return; } else if (permissions[i].equals(Manifest.permission.ACCESS_COARSE_LOCATION) && grantResults[i] == PackageManager.PERMISSION_DENIED) { LOG.d(TAG, "User *rejected* Coarse Location Access"); - this.permissionCallback.error("Location permission not granted."); + callback.error("Location permission not granted."); return; } else if (permissions[i].equals(BLUETOOTH_SCAN) && grantResults[i] == PackageManager.PERMISSION_DENIED) { LOG.d(TAG, "User *rejected* Bluetooth_Scan Access"); - this.permissionCallback.error("Bluetooth scan permission not granted."); + callback.error("Bluetooth scan permission not granted."); return; } else if (permissions[i].equals(BLUETOOTH_CONNECT) && grantResults[i] == PackageManager.PERMISSION_DENIED) { LOG.d(TAG, "User *rejected* Bluetooth_Connect Access"); - this.permissionCallback.error("Bluetooth Connect permission not granted."); + callback.error("Bluetooth Connect permission not granted."); return; } } @@ -1281,14 +1294,12 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int switch(requestCode) { case REQUEST_ENABLE_BLUETOOTH: LOG.d(TAG, "User granted Bluetooth Connect access for enable bluetooth"); - enableBluetooth(permissionCallback); - this.permissionCallback = null; + enableBluetooth(callback); break; case REQUEST_BLUETOOTH_SCAN: LOG.d(TAG, "User granted Bluetooth Scan Access"); - findLowEnergyDevices(permissionCallback, serviceUUIDs, scanSeconds, scanSettings); - this.permissionCallback = null; + findLowEnergyDevices(callback, serviceUUIDs, scanSeconds, scanSettings); this.serviceUUIDs = null; this.scanSeconds = -1; this.scanSettings = null; @@ -1296,32 +1307,34 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int case REQUEST_BLUETOOTH_CONNECT: LOG.d(TAG, "User granted Bluetooth Connect Access"); - connect(permissionCallback, deviceMacAddress); - this.permissionCallback = null; + connect(callback, deviceMacAddress); this.deviceMacAddress = null; break; case REQUEST_BLUETOOTH_CONNECT_AUTO: LOG.d(TAG, "User granted Bluetooth Auto Connect Access"); - autoConnect(permissionCallback, deviceMacAddress); - this.permissionCallback = null; + autoConnect(callback, deviceMacAddress); this.deviceMacAddress = null; break; case REQUEST_GET_BONDED_DEVICES: LOG.d(TAG, "User granted permissions for bonded devices"); - getBondedDevices(permissionCallback); - this.permissionCallback = null; + getBondedDevices(callback); break; case REQUEST_LIST_KNOWN_DEVICES: LOG.d(TAG, "User granted permissions for list known devices"); - listKnownDevices(permissionCallback); - this.permissionCallback = null; + listKnownDevices(callback); break; } } + private CallbackContext popPermissionsCallback() { + final CallbackContext callback = this.permissionCallback; + this.permissionCallback = null; + return callback; + } + private UUID uuidFromString(String uuid) { return UUIDHelper.uuidFromString(uuid); } From 216fe2e692b4a5f11f6a1676cb016bcd79de2275 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Tue, 18 Oct 2022 17:50:38 +1100 Subject: [PATCH 10/11] Restore refreshCallback override behaviour #936 --- src/android/Peripheral.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/android/Peripheral.java b/src/android/Peripheral.java index de18277a..2ad50436 100644 --- a/src/android/Peripheral.java +++ b/src/android/Peripheral.java @@ -384,9 +384,7 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (refreshCallback != null) { refreshCallback.sendPluginResult(result); refreshCallback = null; - } - - if (connectCallback != null) { + } else if (connectCallback != null) { connectCallback.sendPluginResult(result); } } else { From 299d1db76af9deb18f122f549d0775df4dd750a0 Mon Sep 17 00:00:00 2001 From: Philip Peitsch Date: Tue, 18 Oct 2022 17:53:36 +1100 Subject: [PATCH 11/11] Clear refreshCallback when a new connect attempt is made This ensures the refresh callback can't get stuck permanently preventing the connect callback from firing. --- src/android/Peripheral.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/android/Peripheral.java b/src/android/Peripheral.java index 2ad50436..34520183 100644 --- a/src/android/Peripheral.java +++ b/src/android/Peripheral.java @@ -105,6 +105,10 @@ public void connect(CallbackContext callbackContext, Activity activity, boolean currentActivity = activity; autoconnect = auto; connectCallback = callbackContext; + if (refreshCallback != null) { + refreshCallback.error(this.asJSONObject("refreshDeviceCache aborted due to new connect call")); + refreshCallback = null; + } gattConnect();