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

Android: scan not returning devices #1210

Open
4 tasks done
chris-goodchild opened this issue Jun 7, 2024 · 16 comments
Open
4 tasks done

Android: scan not returning devices #1210

chris-goodchild opened this issue Jun 7, 2024 · 16 comments
Labels

Comments

@chris-goodchild
Copy link

chris-goodchild commented Jun 7, 2024

Prerequisites

  • I checked the documentation and FAQ without finding a solution
  • I checked to make sure that this issue has not already been filed
  • I'm sure that question is related to the library itself and not Bluetooth Low Energy or Classic in general. If that so, please post your question on StackOverflow.
  • I'm running the latest version

Question

I am experiencing an issue on Android (tested on OS v12/13) with React Native 0.70.14 - I am running the latest version of react-native-ble-plx

From my logs, I can see that bluetooth is successfully initialized (although initially it always reports PoweredOff, for some reason). Subsequently, I am prompting the user to grant the following permissions:

  • PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
  • PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT
  • PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN

I have confirmed that location fine access is also granted, as are these permissions, but when it comes to the BLE scan I'm not getting any results. The BLE device is in pairing mode, I get no errors or results during the scan. On iOS this all works without issue, it's only on Android.

AndroidManifest.xml contains all of the required permissions as per the documentation and these permissions are confirmed to be granted prior to running the scan. I'm just not sure why the scan is providing no feedback at all. I've reviewed suggestions from other issues but none appear to be applicable here. Do you have any pointers as to what this issue might be? I'm happy to provide further details if needed.

@MrShahzeb-kresus
Copy link

I am facing same
It is taking forever and not calling the callback we gave

any solution?

@mrshahzeb7
Copy link

any solution?

@chris-goodchild
Copy link
Author

Just to add some more info on our particular setup...

AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission
  android:name="android.permission.BLUETOOTH"
  android:maxSdkVersion="30" />
<uses-permission
  android:name="android.permission.BLUETOOTH_ADMIN"
  android:maxSdkVersion="30" />

<!-- Bluetooth runtime permissions for Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Our implementation is prompting the user to grant ACCESS_FINE_LOCATION, BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions. Even after these are granted, the scan doesn't do anything; no callback is fired for either device or error handlers, and unless a manual timeout is implemented, the scan continues running indeterminately.

I have had one or two instances where the BLE state is returned as Resetting which I am now handling (the example implementation doesn't handle this case), however, it doesn't affect anything. Eventually it changes to PoweredOn and allows the scan to go ahead but it's the same outcome.

@kacper-cyra
Copy link
Contributor

Hi, I've tried to reproduce this issue on android v12/13 and I couldn't do it. Can you share the code snippets?

  1. How do you initialize BLE service?
  2. How to scan for devices?
  3. How do you define a callback function?

@mrshahzeb7
Copy link

mrshahzeb7 commented Jun 11, 2024

I have used exactly the same code that is in the example!

Device = nokia 5.4 android 12

I called this function from "example" in repo

startConnectAndDiscover()

It is using await bleservice.scanDevices
and inside that there is ".then"

and there is just a console "scanning" in it

I just see that console in terminal

Nothing else happens!

Copied all the code from example

Using the same Bluetooth service that is on the example

@chris-goodchild
Copy link
Author

Hi, I've tried to reproduce this issue on android v12/13 and I couldn't do it. Can you share the code snippets?

  1. How do you initialize BLE service?
  2. How to scan for devices?
  3. How do you define a callback function?

Here's some example code:

initializeBLE = async () => {
    this.log('Initializing BLE');

    const granted = await this.requestPermissions();

    if (!granted) {
        const error = new Error('Permissions not granted');
        this.clientErrorHandler(error);
        throw error;
    }

    return new Promise<void>(resolve => {
        const subscription = this.manager.onStateChange(state => {
            this.log(`BLE manager state ${state}`);

            switch (state) {
                case State.Resetting: {
                    break;
                }
                case State.Unsupported: {
                    this.clientErrorHandler(new Error(State.Unsupported));
                    break;
                }
                case State.PoweredOff: {
                    this.manager.enable().catch((error: BleError) => {
                        this.log('Enabling bluetooth failed');
                        this.clientErrorHandler(error);

                        if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
                            this.log('Prompting user to accept permissions');
                            this.requestPermissions().then(granted => {
                                this.log(`Bluetooth permissions ${granted ? 'granted' : 'denied'}`);
                            });
                        }
                    });
                    break;
                }
                case State.Unauthorized: {
                    this.requestPermissions().then(granted => {
                        this.log(`Bluetooth permissions ${granted ? 'granted' : 'denied'}`);
                    });
                    break;
                }
                case State.PoweredOn: {
                    resolve();
                    subscription.remove();
                    break;
                }
                default: {
                    this.clientErrorHandler(new Error(`Unsupported BLE state: ${state}`));
                }
            }
        }, true);
    });
};

scanDevices = async (
    onDeviceFound: (device: Device) => void,
    onScanError: (error: BleError) => void,
    UUIDs: UUID[] | null = null,
) => {
    this.manager.startDeviceScan(UUIDs, null, (error, device) => {
        this.log('scan result', { error, device });

        if (error) {
            onScanError(error);
            this.onError(error);
            this.manager.stopDeviceScan();
            return;
        }
        if (device) {
            onDeviceFound(device);
        }
    });
};

I have tried running this in various modes and setting the legacyScan flag but I see no results.

@intent-kacper-cyranowski
Copy link
Collaborator

Hi @chris-goodchild
The sample you have provided seems to work just fine. Can you provide a minimal repro or bigger context, how do you use the provided code inside of React?
The more information you provide the better the chance of solving the problem

@chris-goodchild
Copy link
Author

@intent-kacper-cyranowski sure, so we're using this library in the context of a very large RN app. The scan/connect/write operations are being used to install a custom BLE device. It's integrated within a set of screens guiding a user through connection. I have a BleServiceInstance class based heavily on the one in the provided example app. When the journey is started the instance is created and accessed via React's Context API and the methods are invoked within React components. For example:

const { bleService } = useContext(Context);
const [devices, setDevices] = useState<Device[]>([]);

const onDeviceFound = useCallback(
    (device: Device) => {
        clearTimer();
        setDevices([...devices, device]);
    },
    [clearTimer, devices],
);

const onScanError = useCallback(
    (error: BleError) => {
        clearTimer();
        setError(error);
    },
    [clearTimer],
);

const scanForDevices = useCallback(async () => {
    startTimer();
    await bleService.initializeBLE();
    await bleService.scanDevices(onDeviceFound, onScanError);
}, [startTimer, onDeviceFound, onScanError]);

useEffect(() => {
    scanForDevices();
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

and inside the bleService instance:

initializeBLE = async () => {
    this.log('Initializing BLE');

    await this.requestBluetoothPermission();

    return new Promise<void>(resolve => {
        const subscription = this.manager.onStateChange(state => {
            this.log(`BLE manager state ${state}`);

            switch (state) {
                case State.Resetting: {
                    break;
                }
                case State.Unsupported: {
                    this.clientErrorHandler(new Error(State.Unsupported));
                    break;
                }
                case State.PoweredOff: {
                    this.manager.enable().catch((error: BleError) => {
                        this.log('Enabling bluetooth failed');
                        this.clientErrorHandler(error);

                        if (error.errorCode === BleErrorCode.BluetoothUnauthorized) {
                            this.log('Prompting user to accept permissions');
                            this.requestBluetoothPermission().then(granted => {
                                this.log(`Bluetooth permissions ${granted ? 'granted' : 'denied'}`);
                            });
                        }
                    });
                    break;
                }
                case State.Unauthorized: {
                    this.requestBluetoothPermission().then(granted => {
                        this.log(`Bluetooth permissions ${granted ? 'granted' : 'denied'}`);
                    });
                    break;
                }
                case State.PoweredOn: {
                    resolve();
                    subscription.remove();
                    break;
                }
                default: {
                    this.clientErrorHandler(new Error(`Unsupported BLE state: ${state}`));
                }
            }
        }, true);
    });
};

scanDevices = async (onDeviceFound: (device: Device) => void, onScanError: (error: BleError) => void) => {
    try {
        await this.manager.startDeviceScan(
            null,
            { legacyScan: true, scanMode: ScanMode.LowLatency },
            (error, device) => {
                this.log('scan result', { error, device });

                if (error) {
                    onScanError(error);
                    this.onError(error);
                    this.manager.stopDeviceScan();
                    return;
                }
                if (device) {
                    onDeviceFound(device);
                }
            },
        );
    } catch (error) {
        console.error('scan error', error);
    }
};

requestBluetoothPermission = async () => {
    if (Platform.OS === 'ios') {
        return true;
    }

    if (Platform.OS === 'android') {
        const apiLevel = parseInt(Platform.Version.toString(), 10);

        if (apiLevel < 31 && PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION) {
            const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
            return granted === PermissionsAndroid.RESULTS.GRANTED;
        }

        if (PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN && PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT) {
            const result = await PermissionsAndroid.requestMultiple([
                PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
                PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
            ]);

            return (
                result['android.permission.BLUETOOTH_CONNECT'] === PermissionsAndroid.RESULTS.GRANTED &&
                result['android.permission.BLUETOOTH_SCAN'] === PermissionsAndroid.RESULTS.GRANTED
            );
        }
    }

    this.clientErrorHandler(new Error('Permissions have not been granted'));

    return false;
};

A coworker has gottent the example app working and producing scan results. For whatever reason though, when it's integrated into the app on Android it never returns any scan results. I'm on version 3.2.0 - I have double checked that our manifest has the exact same permissions setup as the example app and as far as I can see all of our implementation code is as it should be. The app requests permissions when needed. The scan starts but it never responds with anything. I'm a bit stumped at this point. I can't share our codebase unfortunately because it's private. Do you have any idea why there would be no response at all? Could this be something related to permissions that I may have missed?

Some logcat output:

2024-06-14 11:40:51.804  2848-2979  ReactNativeJS           com.hivehome.react.android.beta      I  'Initializing BLE', {}
2024-06-14 11:40:51.808  2848-2848  ScrollView              com.hivehome.react.android.beta      D  initGoToTop
2024-06-14 11:40:51.825  2848-2979  ReactNativeJS           com.hivehome.react.android.beta      I  PoweredOn
2024-06-14 11:40:51.826  2848-2979  ReactNativeJS           com.hivehome.react.android.beta      I  'BLE manager state PoweredOn', {}
2024-06-14 11:40:51.827  2848-2979  ReactNativeJS           com.hivehome.react.android.beta      I  'Scanning for BLE devices', {}
2024-06-14 11:40:51.854  2848-3173  BluetoothAdapter        com.hivehome.react.android.beta      D  getBleEnabledArray(): ON
2024-06-14 11:40:51.855  2848-3173  BluetoothAdapter        com.hivehome.react.android.beta      I  BLE support array set: [true, true, true, true, true, true, true]
2024-06-14 11:40:51.855  2848-3173  BluetoothAdapter        com.hivehome.react.android.beta      D  getBleEnabledArray(): ON
2024-06-14 11:40:51.857  2848-3173  BluetoothAdapter        com.hivehome.react.android.beta      D  semIsBleEnabled(): ON
2024-06-14 11:40:51.859  2848-3173  BluetoothAdapter        com.hivehome.react.android.beta      D  getBleEnabledArray(): ON
2024-06-14 11:40:51.859  2848-3173  BluetoothLeScanner      com.hivehome.react.android.beta      D  Start Scan with callback
2024-06-14 11:40:51.863  2848-2869  BluetoothLeScanner      com.hivehome.react.android.beta      D  onScannerRegistered() - status=0 scannerId=7 mScannerId=0
2024-06-14 11:40:51.868  4008-4672  BtGatt.GattService      com.android.bluetooth                E  [GSIM LOG]: gsimLogHandler, msg: MESSAGE_SCAN_START, appName: com.hivehome.react.android.beta, scannerId: 7, reportDelayMillis=0

@chris-goodchild
Copy link
Author

Just to add, on the Samsung device I'm testing, if I initially have bluetooth turned off then the state is PoweredOff but the enable function fails after I enable using the prompt and the caught error is OperationCancelled so the whole process fails.

@intent-kacper-cyranowski
Copy link
Collaborator

I have created a branch with minimal repro so we could be on the same page with the code #1210_android_scan_not_returning_devices. In my opinion to solve the problem I would need the insight to the React context.

I believe that isn’t the problem with library/permissions, but with React/js code itself. I can spot some issues, but not why scan result wouldn’t be called. If the bleService comes from your context how do you initialize it (it shouldn’t be initialized inside of React tree as it does not guarantee that code won’t be re-run, and you should have only one instance of BleManager, so there is no need to use context for it)

@chris-goodchild
Copy link
Author

@intent-kacper-cyranowski thanks for putting the effort in to do that! I checked and really, the React Context aspect isn't a factor. It is initialised at the start of a flow of screens and only gets killed when navigated away from the stack, i.e. completing or cancelling. I tested it without using Context just to be sure but the outcome is the same. As this works fine on iOS I don't believe it's likely to be a problem at the React level, though I took some of your commented suggestions on board, thanks.

A colleague has been doing some debugging and is able to see scan results using the example app after upgrading his dev environment (we needed to upgrade node/java/Android Studio), but integrating the source into our project it seems that even though scan results are there at the native layer, for whatever reason they're not being surfaced to RN. It's as if the event isn't being fired/picked up. We're still digging into it. I'll report back if we find anything.

@chris-goodchild
Copy link
Author

Just an update on this, I believe our issue may have been related to permissions and how we were linking the library. I can't be sure as it suddenly started working after numerous attempts to clean and rebuild. Our project is very large and we still manually link some libraries (because reasons). At the moment, using the example for initializing BLE, i.e. the bluetooth state subscription, the manager.enable call fails with the BleError OperationCancelled. I have seen the prompt to enable bluetooth but it seems to fail at that point and doesn't trigger the permissions request as it's within a conditional checking for the BluetoothUnauthorized error.

@intent-kacper-cyranowski
Copy link
Collaborator

intent-kacper-cyranowski commented Jul 4, 2024

Looking at the code, the only case for receiving OperationCancelled on manager.enable is to cancel subscription manually or by creating new instance of BleManager (in effect of that it could drop subscription).

Showing relevant code or some Logcat logs after setting setLogLevel to LogLevel.Verbose, and filtering them with package:mine (tag:BluetoothGatt | tag:ReactNativeJS | RxBle) should provide more insight to the problem

@intent-kacper-cyranowski
Copy link
Collaborator

Hi @chris-goodchild
Did you manage to fix the issue? If not, can I help in any way?

@akhockey21
Copy link

I'm having similar issues. Please don't close this issue, I will report back with any findings or fixes I have. Thanks

@chris-goodchild
Copy link
Author

chris-goodchild commented Oct 9, 2024

Apologies for the (very) late reply @intent-kacper-cyranowski . Yes, we're still seeing the issue intermittently. Also seeing frequent disconnects on both iOS and Android. On Android specifically, sometimes we get results but about half the time we get nothing at all. In general, if we do manage to find the device and connect to it, the connection is super unstable. It seems like reconnect attempts sometimes work but in most cases (again, both platforms) everything needs to be destroyed and started again. It's super hard to see what's going on though. I'm not sure if the isDeviceConnected method is particularly reliable either. Before writing a characteristic I check the connection status using this method and it returns true, but then the attempted write results in a combination of cancelled and disconnected errors. It's as if the ble-plx library gets out of sync with the actual device connection or something.

We have been testing our BLE device using other bluetooth apps and it's only ble-plx that seems to have this issue. In all other tests the connection is persistent but in the app using ble-plx the connection drops frequently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants