flutter_reactive_ble icon indicating copy to clipboard operation
flutter_reactive_ble copied to clipboard

[Android] pairing request popup appears twice

Open BradKwon opened this issue 2 years ago • 21 comments

Describe the bug When a bluetooth peripheral, which has a Nordic chip (I am not sure this happens only with this chip though), is connected to some Android phones, the Android native pairing request popup appears twice. The first popup does nothing.

To Reproduce Steps to reproduce the behavior:

  1. Run example Android app
  2. Connect the bluetooth peripheral, possibly with a Nordic chip.
  3. Check if Android native pairing request popup appears.
  4. Click "pair" button on it.
  5. Observe the 2nd pairing popup appears.

Expected behavior The Android native pairing popup appears only once.

  • [x] I tried doing the same with a general BLE scanner application (e.g. nRF Connect) and it exhibits the expected behavior as described above

Smartphone / tablet

  • Device: Google Pixel 4a
  • OS: Android 12
  • Package version: 5.0.1

Peripheral device

  • Vendor, model: Custom with Nordic chip
  • Does it run a custom firmware / software: no

Additional context This also happens with the general BLE scanner app however, I am wondering if there are any workaround for it.

BradKwon avatar Jan 27 '22 14:01 BradKwon

Can you reproduce the issue with any other BLE device than the custom Nordic chip? As we're not seeing this issue in the field, it might have to do with your device (even though the nRF Connect app doesn't show it).

And to be precies; what "general BLE scanner app" do you mean?

PieterAelse avatar Feb 02 '22 12:02 PieterAelse

@PieterAelse I have tested it again with other BLE mouse. I think this seems not a Nordic chip. I was not able to find which chip this has. The pairing request popup appears twice as well with it.

I noticed that it appears only once on my two Android 9 devices, but on the Android 12 device, Google Pixel 4a.

My Android 9 devices: Motorola X4 LG G2 w/ Lineage OS

I mean the nRF Connect app by "general BLE scanner app". Sorry for the confusion. I just used the term in the template.

Based on my further test, the issue only happens on Android 12. Unfortunately, I have only Google Pixel 4a so cannot test it on other Android 12 devices. And the nRF Connect app also shows it twice so it looks like a general issue on this device or Android version.

It would be nice someone else report this or can test it on other Android 12 devices. And it would be nice if there are any workaround.

BradKwon avatar Feb 03 '22 13:02 BradKwon

I have the same thing happening to me. On a Pixel 4A5G with Android 12. If I don't accept it twice, it's not working afterwards. I try my code on two other phone :

  • Pixel 4A with Android 11
  • RugGear with *Android 8 And I don't have this issue of "double pop up". So it looks like it is related to android 12.

GaelleJoubert avatar Feb 09 '22 08:02 GaelleJoubert

I have observed a similar issue on the Pixel 5 with all current security patches.

I have an additional request in that when I connect to my device it is necessary to set up notifications for some of the characteristics. Is there a way of determining that the user has completed the bonding process and entered a valid pin before performing device discovery and characteristic notification setup? Waiting for connected state alone will cause an exception if the pairing process is not complete. This is only a problem on Android, iOS is handled properly.

jhewitt avatar Mar 08 '22 15:03 jhewitt

@jhewitt Have your find any solution actually, I'm also facing same issue. If found, it's helpful for everyone.

gourav6m17 avatar Apr 25 '22 06:04 gourav6m17

The double pairing popup is an Android bug. It happens when the remote peripheral initiates the bonding process. The workaround is to initiate bonding yourself. This can be done on Android using createBond() but it cannot be done iOS.

martijnvanwelie avatar Apr 25 '22 08:04 martijnvanwelie

@martijnvanwelie Thanks for the comment. I thought it would be good to get this around via this package however, it seems that there is no workaround as I haven't got any feedback here since Feb. 2.

Then is it possible for you to provide me with how I can initiate the Android bonding with this package in Flutter?

BradKwon avatar Apr 30 '22 08:04 BradKwon

The same issue also happens when connecting to an ESP32 where the characteristics access requires pairing/bonding. Based on the discussion above, it sounds like adding the option to manually call the createBond() method on Android may resolve the issue?

dusalex avatar Jun 15 '22 07:06 dusalex

Hi @PieterAelse, I already answered back in Feb. and the label says still "awaiting response". Do you have any ideas what is the problem here?

BradKwon avatar Aug 24 '22 12:08 BradKwon

The same issue also happens when connecting to an ESP32 where the characteristics access requires pairing/bonding. Based on the discussion above, it sounds like adding the option to manually call the createBond() method on Android may resolve the issue?

We would rather not, because we don't want to have platform code specific inside this library.

Taym95 avatar Oct 24 '22 14:10 Taym95

@Taym95 Hmm... there are already Android-specific operations, as listed in the docs, e.g., requestMtu, requestConnectionPriority, clearGattCache. As there are so many difference regarding BLE between Android and iOS, a library addressing this topic somehow needs to be able to properly manage them in my opinion.

dusalex avatar Oct 24 '22 20:10 dusalex

So how should this problem be solved? How to manage them properly, you didn't make it clear, I don't know how to do it? Looking forward to your reply

paintingStyle avatar Dec 09 '22 01:12 paintingStyle

I am having the same issue. Any way to solve it?

I don't see any docs or even comments anywhere regarding createBond()...

pzehle avatar Dec 21 '22 00:12 pzehle

Ist there any update to this issue? I am facing the same issue.

ben91187 avatar Mar 15 '23 15:03 ben91187

This issue was reproduced with several embedded devices: espressif ESP32 and Thunderboard Sense2. Also with a host of other devices Motorola G30, Huawei P30 Light, all with Android Version 12. We could find a workaround on the embedded device side for this. The issues , or the second pop-up is appearing when you enable encryption on the GATT server characteristics. If you disable encryption requirement, the second pop-up disappears. So android when discovers charactheristichs that need encryption pushes another pop-up. Which is not displayed all the time. Also the device appears in the bonded list even though the device is set up to only pair. This workaround is not a proper fix. We are reducing security requirement only to avoid this problem. However people facing this issue could in theory avoid it like this.

exilat avatar Apr 21 '23 13:04 exilat

I would also like to have a solution here. And there is already platform-specific code in this library anyway. Ideally, you could argue that you don't want platform-specific workflows instead of no platform-specific code.

clemens- avatar Jun 13 '23 09:06 clemens-

@exilat I am facing the same problem with android 13 and android 12 device. Please suggest any solution to fix this issue.

SatyendraM1990 avatar Aug 30 '23 15:08 SatyendraM1990

I circumvented this problem by connecting to the device and then waiting until the device shows up in the paired devices list. This means only one pairing request will be sent, after that the app is sent into a loop until the user has paired the device. It's crude, but it works. I will still need to add a loop escape sequence. I used a Method Channel to do that. Here is how it looks like in code:

Flutter code for connecting and waiting on pairing state:

                // Trigger bonding on Android
                if (Platform.isAndroid) {
                  bool paired =
                      await Provider.of<BleService>(context, listen: false)
                          .checkPairingState();
                  print("Pairing state: $paired");
                  if (paired == false) {
                    await Provider.of<BleService>(context, listen: false)
                        .connectMyDevice();
                    do {
                      paired =
                          await Provider.of<BleService>(context, listen: false)
                              .checkPairingState();
                    } while (paired == false);
                    await Provider.of<BleService>(context, listen: false)
                        .disconnect();
                  }
                }

checkPairingState method

  static const platform = MethodChannel('ch.myapp.app/bluetooth');

  Future<bool> checkPairingState() async {
    return true;
    try {
      var pairedDevices = await platform.invokeMethod('getPairedDevices');
      print("pairedDevices: $pairedDevices");
      for(String pairedDevice in pairedDevices) {
        if(pairedDevice == _prefs.getString('deviceId')) {
          return true;
        }
      }
    } on PlatformException catch (e) {
      print('PlatformException: ${e.code} ${e.message}');
    }
    return false;
  }

MainActivity.kt

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "ch.myapp.app/bluetooth").setMethodCallHandler {
                call, result ->
            if(call.method == "getPairedDevices") {
                val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
                val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
                if (bluetoothAdapter == null) {
                    result.notImplemented()
                }
                if (ActivityCompat.checkSelfPermission(
                        this,
                        Manifest.permission.BLUETOOTH_CONNECT
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    result.error("Permission missing", "BLUETOOTH_CONNECT not granted", null)
                } else {
                    val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
                    val pairedDevicesList: MutableList<String> = ArrayList()
                    pairedDevices?.forEach { device ->
                        pairedDevicesList.add(device.address.toString())
                    }
                    result.success(pairedDevicesList)
                }
            }
            else {
                result.notImplemented()
            }
        }
    }

clemens- avatar Aug 31 '23 07:08 clemens-

@clemens- can you give a more details regarding the dart calls? For example, can you share the connectMyDevice() method of your BleService? I'm trying to implement a similar logic but I still got 2 popups

mrpurpleknight avatar Oct 18 '23 16:10 mrpurpleknight

@mrpurpleknight the trick is to start connecting so that a pairing request is triggered, but then periodically check if it has been accepted. I added some more comments here:

                // Trigger bonding on Android
                if (Platform.isAndroid) {
                // Check the initial pairing state
                  bool paired =
                      await Provider.of<BleService>(context, listen: false)
                          .checkPairingState();
                  print("Pairing state: $paired");
                  // If we are already paired, we can skip this next section
                  if (paired == false) {
                    // If we are not yet paired, start connecting to trigger a pairing request
                    await Provider.of<BleService>(context, listen: false)
                        .connectMyDevice();
                    // Now enter a loop until the user has paired the device. 
                    // This prevents more pairing requests in the meantime
                    do {
                      paired =
                          await Provider.of<BleService>(context, listen: false)
                              .checkPairingState();
                    } while (paired == false);
                    // Disconnect, as I only want to pair at this stage.
                    // The next Widget can always start with a disconnected and paired device
                    await Provider.of<BleService>(context, listen: false)
                        .disconnect();
                  }
                }

Note that for good practise you should add a timer or something that prevents the loop from running for all eternity. That I have not done yet here.

Also, here is the rest of the code you requested, but I don't think it will help you, as all the magic is in the code already posted:

// Connect to the stored device
  Future<void> connectMyDevice() async {
    String? deviceId = _prefs.getString('deviceId');
    if (deviceId == null) {
      return;
    }
    connect(deviceId);
  }

  // Connect to a device
  Future<void> connect(String deviceId) async {
    print('Start connecting to $deviceId');
    _prefs.setString('deviceId', deviceId);
    await cancelNotifications();
    _connection = _flutterReactiveBle.connectToDevice(id: _prefs.getString('deviceId')!).listen(
      (update) async {
        _connectionStateUpdateHandler(update.connectionState);

        if (update.connectionState == DeviceConnectionState.connected) {
          _setCharacteristics();
        }
      },
      onError: (Object e) {
        print('Connecting to device ${_prefs.getString('deviceId')} resulted in error $e');
      },
    );
  }

clemens- avatar Oct 19 '23 14:10 clemens-

This seems need to be done on BLE peripheral device side, since almost every BLE communication will enable charateristic encryption, it will force the central device send out Pairing Request, while if BLE peripheral device send Security Request after connection up, it will trigger another Pairing Request, testing with TI CC2642R2 device, if I remove the Security Request, then only one Pairing pops out.

dianke0201 avatar Apr 11 '24 02:04 dianke0201