RxAndroidBle icon indicating copy to clipboard operation
RxAndroidBle copied to clipboard

Library should provide helper for binding devices

Open klemzy opened this issue 8 years ago • 19 comments

Summary

When pairing device (initiating connection for the first time) bluetooth device requests for PIN which is displayed as notification. Another approach is to display this request as popup dialog. Does the library support displaying popup dialog for PIN input? I can try and submit PR if not.

Kind Regards

klemzy avatar Jun 22 '16 19:06 klemzy

Didn't think we could control that? Or maybe I am wrong. I guess I am wrong since you have a PR ready for it :)

jssingh avatar Jun 22 '16 19:06 jssingh

I don't have it ready yet, I was just wondering if library already supports this, otherwise I can find I way to support this because I would really need this feature :) In my experience by using BluetoothDevice API with createBond then you get presented with popup dialog.

klemzy avatar Jun 22 '16 20:06 klemzy

Bluetooth Low Energy generally does not require bonding/pairing and the library doesn't support that. Why do you need to create a bond with BLE device?

uKL avatar Jun 23 '16 06:06 uKL

I see. Well we have a BLE device which requires pairing becase of security issues. And if initiating connection without specific bond request pairing is displayed as notification and not popup dialog. If this feature is not scope of this library I will just handle this internally. No problem.

klemzy avatar Jun 23 '16 09:06 klemzy

I'll keep this issue and we will discuss internally whether it is within the scope. Thank you for your feedback! :)

uKL avatar Jun 23 '16 09:06 uKL

I just noticed that in SHAPSHOT version, scan already exposes BluetoothDevice but it's not released yet. This actually solves my problem.

klemzy avatar Jul 05 '16 18:07 klemzy

How does scan expose BluetoothDevice? I can't find it in 1.1.0-SNAPSHOT. As far as I can tell, scan returns RxBleScanResults, which don't expose BluetoothDevice. I'm trying to bond to a device and it would be nice if I didn't have to get a reference to the adapter.

jtomazin avatar Jul 18 '16 14:07 jtomazin

RxBleScanResult.getBleDevice().getBluetoothDevice()

dariuszseweryn avatar Jul 18 '16 14:07 dariuszseweryn

@klemzy "...And if initiating connection without specific bond request pairing is displayed as notification and not popup dialog...." - I also run into this issue and I found out, that I need to start and stop discovery on my device: BluetoothAdapter.getDefaultAdapter().startDiscovery() -> with a change receiver stop and after that, I do a connect, which always shows the popup and not the notification.

mars3142 avatar Dec 08 '16 14:12 mars3142

The biggest problem is the comment in the NordicSemiconductor (Android-nRF-Toolbox) repo (https://github.com/NordicSemiconductor/Android-nRF-Toolbox/blob/master/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleManager.java#L1050), which seems to also affected your app:

/*
* The onConnectionStateChange event is triggered just after the Android connects to a device.
* In case of bonded devices, the encryption is reestablished AFTER this callback is called.
* Moreover, when the device has Service Changed indication enabled, and the list of services has changed (e.g. using the DFU),
* the indication is received few milliseconds later, depending on the connection interval.
* When received, Android will start performing a service discovery operation itself, internally.
*
* If the mBluetoothGatt.discoverServices() method would be invoked here, if would returned cached services,
* as the SC indication wouldn't be received yet.
* Therefore we have to postpone the service discovery operation until we are (almost, as there is no such callback) sure, that it had to be handled.
* Our tests has shown that 600 ms is enough. It is important to call it AFTER receiving the SC indication, but not necessarily
* after Android finishes the internal service discovery.
*
* NOTE: This applies only for bonded devices with Service Changed characteristic, but to be sure we will postpone
* service discovery for all devices.
*/

I ran into the issue with my BLE device, which can't reconnect and I think, it's because of the caching. Am I able to start this discovery by myself for bonded devices?

mars3142 avatar Dec 08 '16 14:12 mars3142

I need bonding/pairing because it gives secure/encrypted BLE connection.

jeffreyliu8 avatar Mar 24 '17 00:03 jeffreyliu8

Version 1.3.0-SNAPSHOT is no longer closing the RxBleConnection if an error happens during a connection operation (i.e. RxBleConnection.characteristicRead) which allows for a retry which may succeed as Android is sometimes reestablishing encryption semi-transparently (it errors only once). This should be enough for working with encrypted connections. The BluetoothDevice for .createBond() is already exposed.

I do not have much access to peripherals that need bonding / encryption to work and I can only test these features by implementing them by myself on some dev boards. I am not a big fan of testing a library against my own implementation. I would be grateful if someone could explain the Android flow for bonding and establishing encryption which could be implemented in the library.

dariuszseweryn avatar Jun 05 '17 17:06 dariuszseweryn

Essentially when you are deciding to connect to the device you check to see if it is in the list of bonded devices, and if so you can immediately connect. If the device has yet to be bonded then you make the request to bond to the device and then register a BroadcastReceiver for specific Bluetooth intents to know when the device has been bonded so then you can connect to it.

Connection:

BluetoothManager bluetoothManager = (BluetoothManager) context
        .getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();

if (bluetoothAdapter.getBondedDevices().contains(device)) {
    // connect to device
} else if (device.createBond()) {
    registerBluetoothIntents();
}

Register for receiver:

private void registerBluetoothIntents() {
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
    intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);  // really the only important one
    intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
    intentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
    intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);

    context.registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            BluetoothDevice device;

            switch (action) {
                case BluetoothDevice.ACTION_FOUND:
                    break;
                case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
                    device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                    int prevState = intent
                            .getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, 0);
                    int bondState = device.getBondState();

                    if (bondState == BluetoothDevice.BOND_BONDING) {
                        // bonding
                    } else if (bondState == BluetoothDevice.BOND_BONDED) {
                        // bonded

                        if (prevState == BluetoothDevice.BOND_BONDING) {
                            // connect to device
                        }
                    } else if (bondState == BluetoothDevice.BOND_NONE) {
                        if (prevState == BluetoothDevice.BOND_BONDING) {
                            // error
                        }
                    }

                    break;
                case BluetoothAdapter.ACTION_SCAN_MODE_CHANGED:
                    break;
                case BluetoothAdapter.ACTION_STATE_CHANGED:
                    break;
                case BluetoothDevice.ACTION_PAIRING_REQUEST:
                    break;
            }
        }
    }, intentFilter);
}

lorenzowoodridge avatar Jun 22 '17 14:06 lorenzowoodridge

Thank you @lorenzowoodridge Thing is that there is a tone of bugs on Android OS regarding bonding BLE devices. I have tried to bond Android 7.1.2 with Android 4.4 / 6.0.1 and one more that I cannot remember now. I have only encountered bugs:

  • 4.4 was disconnecting after about 1.5 seconds of link inactivity (no reads/writes) and was loosing the bond on one of the sides randomly after several connections
  • 6.0.1 could not connect until adapter off/on cycle when it started to connect but it also was disconnecting after 1.5 seconds of link inactivity There is a quite significant list of bond related Android bugs. Until we will come up with some stable workarounds there is no point (in my opinion) to add this functionality to the library itself as it will only make more people complain that something is not working.

Every idea of how to workaround those issues is welcome.

dariuszseweryn avatar Jun 23 '17 07:06 dariuszseweryn

I don't know if I could be helpful. With my device I have got a lot of issues related to the bonding with Android, with the firmware team we didn't understand why it was asking a pairing code on Android but with iOS is working without. In the firmware there is a "Just Work" implementation, so it shouldn't ask for a code, but for some unknown (yet) reasons the handshake with Android is different.

So I tried to manage with code in order to avoid to ask to the user the PIN and I came out with this (I've found on the net I don't remember where and I slightly edit it):

public abstract class BondReceiver extends BroadcastReceiver {
    private static final String TAG;
    private WeakReference<Context> mContextWeakReference;

    public abstract void onDeviceBonded(String str);

    public abstract void onDeviceUnbonded(String str);

    static {
        TAG = BondReceiver.class.getSimpleName();
    }

    public BondReceiver(Context context) {
        this.mContextWeakReference = null;
        if (context == null) {
            throw new IllegalArgumentException("'context' cannot be null!");
        }
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("android.bluetooth.device.action.BOND_STATE_CHANGED");
        intentFilter.addAction("android.bluetooth.device.action.PAIRING_REQUEST");
        intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
        context.registerReceiver(this, intentFilter);
        this.mContextWeakReference = new WeakReference<>(context);
    }

    public void onReceive(Context context, Intent intent) {
        if (intent == null) {
            return;
        }
        String action = intent.getAction();
        if ("android.bluetooth.device.action.BOND_STATE_CHANGED".equals(action)) {
            handleBondingEvents(intent);
        } else if ("android.bluetooth.device.action.PAIRING_REQUEST".equals(action)) {
            handlePairingRequest(intent);
        }
    }

    private void handleBondingEvents(Intent intent) {
        int prevBondState = intent.getIntExtra("android.bluetooth.device.extra.PREVIOUS_BOND_STATE", -1);
        int bondState = intent.getIntExtra("android.bluetooth.device.extra.BOND_STATE", -1);
        BluetoothDevice device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
        Timber.v("device = " + device.getAddress() + " prev state = " + prevBondState + " new state = " + bondState);
        if (prevBondState == BluetoothDevice.BOND_BONDED && bondState == BluetoothDevice.BOND_NONE) {
            onDeviceUnbonded(device.getAddress());
        } else if (prevBondState == BluetoothDevice.BOND_BONDING && bondState == BluetoothDevice.BOND_BONDED) {
            onDeviceBonded(device.getAddress());
        } else if (prevBondState == BluetoothDevice.BOND_BONDING && bondState == BluetoothDevice.BOND_NONE) {
            onDeviceUnbonded(device.getAddress());
        }
    }

    private void handlePairingRequest(Intent intent) {
        BluetoothDevice device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
        if (device == null) {
            Timber.w("handlePairingRequest: device is null");
        } else {
            int type = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
            Timber.d("Pairing type: " + type);
            if (type == BluetoothDevice.PAIRING_VARIANT_PIN) {
                String pin = "123456";
                device.setPin(pin.getBytes());
                abortBroadcast();
            }
        }
    }

    public void stopReceiving() {
        Context context = this.mContextWeakReference.get();
        if (context != null) {
            try {
                context.unregisterReceiver(this);
            } catch (Exception e) {
                Timber.e(e, e.getMessage());
            }
        }
    }
}

Basically, as I understand in my case, the bonding process calls android.bluetooth.device.action.PAIRING_REQUEST two time, one for asking the PIN and the other one for asking confirm to the user. With that class I intercept the PIN request and set it via code, the pairing confirmation it still redirect to the user (but I noticed that in a lot of cases is not requested).

pregno avatar Aug 25 '17 14:08 pregno

Try as I may, I can't find a completely clear explanation of BLE pairing and bonding (complicated by the fact that many people apparently misuse the terms). "Pairing" seems to be part of the process that happens when 2 BLE devices connect, and involves the generation and exchange of encryption keys. True? "Bonding" involves the devices storing the keys for use later, so that on subsequent connections they can skip the initial pairing handshake. True? And yet @uKL says (above): "Bluetooth Low Energy generally does not require bonding/pairing and the library doesn't support that." Very confused. I'd be very grateful if someone could point me to a full explanation.

RobLewis avatar Sep 28 '17 14:09 RobLewis

Hello @RobLewis

From what I know you are correct with the difference between pairing and bonding. @uKL is also right saying that BLE does not require pairing/bonding to communicate (as long as no encryption is needed).

Why the library does not support pairing/bonding? There is a ton of bugs related to it and most likely all questions and asking for help would end up as issues. These are hard to debug and would consume a lot of time to investigate as most of them are OS/model specific. Time is a scarce resource for me and I still have a lot of development to do not including support of pairing/bonding.

I hope that this explains your doubts.

dariuszseweryn avatar Sep 28 '17 18:09 dariuszseweryn

~~Hi @RobLewis - i think pairing/bonding are the other way around - bonding is key exchange, pairing is saving keys~~

tristandl avatar Feb 22 '18 08:02 tristandl

@dariuszseweryn Is there any plan for this? This is something we have coming down the pipe that we will need to do. in our case, the PIN will be related to the device itself, mostly likely retrieved with a call to a server API. We want to bypass the prompt itself and set the PIN in code.

I've done this with the another library in a sample application using this library: https://github.com/NordicSemiconductor/Android-BLE-Library/

But basically it involved calling createBond() followed by a connect(). You then have to intercept the pairing request to call device.setPIN():

class MyBleManager(context: Context, handler: Handler) : BleManager(context, handler) {
    override fun getGattCallback(): BleManagerGattCallback = object : BleManagerGattCallback() {
        override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
            return true
        }

        override fun onDeviceDisconnected() {

        }
    }

    override fun onPairingRequestReceived(device: BluetoothDevice, variant: Int) {
        super.onPairingRequestReceived(device, variant)
        Log.d(LOG_TAG, "Setting PIN")
        device.setPin(BLUETOOTH_PIN.toByteArray())
    }
}

And then to start the process:

bluetoothDevice.createBond()

bleManager.connect(bluetoothDevice)
                    .done {
                         onStatusChange(ConnectionStatus.CONNECTED)
                     }.enqueue()
                                                                

I'd love to be able to do this with RxAndroidBle which we already use, rather than introducing another library just to do this PIN pairing.

It looks like way under the covers that it's using a BroadcastReceiver, which in turn ends up calling the setPin function.

		context.registerReceiver(mPairingRequestBroadcastReceiver,
				// BluetoothDevice.ACTION_PAIRING_REQUEST
				new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST"));

mattinger avatar Jan 05 '22 19:01 mattinger