Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up various NPEs and exceptions on Android #938

Merged
merged 11 commits into from
Oct 18, 2022
Merged
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 81 additions & 30 deletions src/android/BLECentralPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -117,7 +118,6 @@ public class BLECentralPlugin extends CordovaPlugin {
private static final int REQUEST_ENABLE_BLUETOOTH = 1;

BluetoothAdapter bluetoothAdapter;
BluetoothLeScanner bluetoothLeScanner;

// key is the MAC Address
Map<String, Peripheral> peripherals = new LinkedHashMap<String, Peripheral>();
Expand All @@ -136,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;
Expand Down Expand Up @@ -193,7 +194,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;
Expand All @@ -212,8 +212,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)) {
Expand Down Expand Up @@ -710,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);
Expand Down Expand Up @@ -738,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
Expand Down Expand Up @@ -1081,6 +1092,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);
Expand Down Expand Up @@ -1108,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.");
Expand All @@ -1129,7 +1159,7 @@ private void findLowEnergyDevices(CallbackContext callbackContext, UUID[] servic
}

discoverCallback = callbackContext;
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
List<ScanFilter> filters = new ArrayList<ScanFilter>();
if (serviceUUIDs != null && serviceUUIDs.length > 0) {
for (UUID uuid : serviceUUIDs) {
Expand All @@ -1138,24 +1168,32 @@ 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() {
LOG.d(TAG, "Stopping Scan");
bluetoothLeScanner.stopScan(leScanCallback);
}
}, scanSeconds * 1000);
stopScanHandler.postDelayed(this::stopScan, scanSeconds * 1000);
}

PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}

private void stopScan() {
stopScanHandler.removeCallbacks(this::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 {
Expand Down Expand Up @@ -1212,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)
Expand All @@ -1220,70 +1272,69 @@ 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;
}
}

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;
break;

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);
}
Expand Down
7 changes: 4 additions & 3 deletions src/android/L2CAPContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 13 additions & 5 deletions src/android/Peripheral.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -229,11 +233,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);
}
Expand Down Expand Up @@ -378,9 +388,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 {
Expand Down
21 changes: 18 additions & 3 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare namespace BLECentralPlugin {
service: string;
characteristic: string;
properties: string[];
descriptors?: any[] | undefined;
descriptors?: any[];
}

interface PeripheralData {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down