Monday, March 23, 2015

Scanning for Bluetooth devices in Android

In this post I'm going to outline how you can prompt the user to enable/turn on bluetooth in your Android device, how to show already paired bluetooth devices, and how to scan for new bluetooth devices in range.

For this tutorial, you'll need a physical Android device (I haven't found a way to make the emulator proxy the computer's bluetooth signal), and I guess at least another bluetooth device in range for you to test things.
The source code for this Android Studio project is linked at the bottom.

Note: This tutorial DOES NOT explain how to pair, connect and send data to/from another Bluetooth device. I might make another post for those specific subjects.
Also, this tutorial is based on regular Bluetooth and not Bluetooth Low Energy (LE)

Turn on Bluetooth

While a user can turn Bluetooth on from multiple places or ways in an Android device, one of the issue I had on a client's project was the need for a unified way to show the user how to turn on Bluetooth in their android device. So instead of making X number of screenshots for "Getting started" guides depending on the OS level and the skin of the device, this approach is mainly universal (sure, the dialog might look slightly different on a different OS level)

So, the plan is to have a button in the app that says something like "Enable Bluetooth" and prompts the user for permission to enable Bluetooth:
In here Bluetooth is disabled

After selecting the "Enable BT" in the app, the user gets prompt for permission to turn on Bluetooth. 
Now Bluetooth is enabled on this device

This is super easy, so let's get started.

The first thing you need to do is get the permissions in the Manifest.xml file to access Bluetooth:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Then in our activity, which in my case will be the MainActivity.java, we need to create a few variables:
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

public class MainActivity
{
    private String LOG_TAG; // Just for logging purposes. Could be anything. Set to app_name
    private int REQUEST_ENABLE_BT = 99; // Any positive integer should work.
    private BluetoothAdapter mBluetoothAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LOG_TAG = getResources().getString(R.string.app_name);
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }
}

What did I do here?
  1. I create a constant String named LOG_TAG with just the name of the app to use it in LogCat
  2. We will be starting the Bluetooth adapter with an intent and we will need a number for the request in the Intent. Because of this I created a variable named REQUEST_ENABLE_BT with a number of 99. Any positive integer (unsigned int) should work.
  3. Create a new BluetoothAdapter variable, and instantiate it once the app starts to get the defaultAdapter() from the BluetoothAdapter.
With that, my "Enable BT" button calls a local method to enable Bluetooth on the device, like this:
private void enableBluetoothOnDevice()
{
    if (mBluetoothAdapter == null)
    {
        Log.e(LOG_TAG, "This device does not have a bluetooth adapter");
        finish();
        // If the android device does not have bluetooth, just return and get out.
        // There's nothing the app can do in this case. Closing app.
    }

    // Check to see if bluetooth is enabled. Prompt to enable it
    if( !mBluetoothAdapter.isEnabled())
    {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
}

In here:
  1. I check to see if the varible from the BluetoothAdapter.getDefaultAdapter() (so the mBluetoothAdapter variable) is null. If it is, it means this Android device does not have Bluetooth. In my case I'm logging this as an error and I'm closing the app. There's no point on doing a tutorial on Bluetooth using a device without Bluetooth.
  2. If there is a Bluetooth adapter, I check if it's not enabled (!mBluetoothAdapter.isEnabled()). If the device is not enabled, we need to turn it on.
  3. Create a new Intent that calles the BluetoothAdapter.ACTION_REQUEST_ENABLE intent, start it with the arbitrary variable of 99 created in the previous step.
 At this point you have succesfully prompt the user to enable Bluetooth!

But, umm.....how do we know if the user says no?

Since we started the Intent to start turn on Bluetooth with the "startActivityForResult", we can then get a result back using the "onActivityResult" method, like this:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_ENABLE_BT)
    {
        if (resultCode == 0)
        {
            // If the resultCode is 0, the user selected "No" when prompt to
            // allow the app to enable bluetooth.
            // You may want to display a dialog explaining what would happen if
            // the user doesn't enable bluetooth.
            Toast.makeText(this, "The user decided to deny bluetooth access", Toast.LENGTH_LONG).show();
        }
        else
            Log.i(LOG_TAG, "User allowed bluetooth access!");
    }
}

Since this is a default Android method I'm overriding it. I then check if the requestCode that comes back is the same 99 I created, and if it is I know this answer is coming from the user prompt for Bluetooth I just created.
If the resultCode of this request is 0, it means the user selected no. At this point its up to you what to do but in my case I'm giving a Toast dialog blaming the user for pushing the wrong button.
Any other resultCode means the user accepted the promp.

And...we're done with the prompt to turn on Bluetooth!
Moving on to the next portion of this tutorial.

Show list of already paired (bonded) Bluetooth devices

Pairing with a device is usually a one time thing, but connecting with already paired devices is a recurring thing.
Luckily for us the OS keeps a list (a Set technically...) of all of the Bluetooth devices this Android device has already paired with, and some information out of them. In this portion we're going to show a list of already paired devices and what information the OS is giving us:
Note: different devices will show different information, so don't worry much about null values at this point.

What you see above is what my Note 4 device has been paired with. The first is a Nexus 7 tablet, and the second one is a music player. The information displayed here is whatever the OS already knows of the devices, which is not a lot.

In my app I have a second button for displaying already paired devices, which calls a local method named "getArrayOfAlreadyPairedBluetoothDevices()", which has this block of code:
private ArrayList getArrayOfAlreadyPairedBluetoothDevices()
{
    ArrayList <BluetoothObject> arrayOfAlreadyPairedBTDevices = null;

    // Query paired devices
    Set <BluetoothObject> pairedDevices = mBluetoothAdapter.getBondedDevices();
    // If there are any paired devices
    if (pairedDevices.size() > 0)
    {
        arrayOfAlreadyPairedBTDevices = new ArrayList<BluetoothObject>();

        // Loop through paired devices
        for (BluetoothDevice device : pairedDevices)
        {
            // Create the device object and add it to the arrayList of devices
            BluetoothObject bluetoothObject = new BluetoothObject();
            bluetoothObject.setBluetooth_name(device.getName());
            bluetoothObject.setBluetooth_address(device.getAddress());
            bluetoothObject.setBluetooth_state(device.getBondState());
            bluetoothObject.setBluetooth_type(device.getType());    // requires API 18 or higher
            bluetoothObject.setBluetooth_uuids(device.getUuids());

            arrayOfAlreadyPairedBTDevices.add(bluetoothObject);
        }
    }

    return arrayOfAlreadyPairedBTDevices;
}

Let's break this down:
  1. I created a new BluetoothObject class to create an object that I can pass around with data
  2. I also know my final product is going to be a list of BluetoothObjects, so I need to make this method return an ArrayList of BluetoothObjects
  3. I then ask the mBluetoothAdapter to give me the set of already bonded devices (mBluetoothAdapter.getBondedDevices())
  4. I check to see if the set contains at least 1 element.
  5. I then iterate the set of elements, extract each BluetoothDevice (OS provided type), and then set the values of my custom BluetoothObject with the values of the BluetoothDevice
  6. Finally I add my custom BluetoothDevice to a new ArrayList I created, and I return the ArrayList
And that is also all we need to do to get the list of already bonded/paired devices.
Again, this is super easy.

Finally we will scan for new Bluetooth devices in range.

Scan for Bluetooth devices in range

Out of the whole project this is the most complicated section, but even then its super easy as well.
There are a few things you need to know before we start this section:
  1. You can only scan for other Bluetooth devices that are already set to be discoverable.
  2. Scanning for Bluetooth devices is Asynchronous, which means the OS will do this in the background at its own speed and rate
  3. We can only scan for X amount of time (currently 12 seconds) and then the discovery ends. Restarting the discovery usually works but if you restart it many times in a row without a break, the OS returns weird Bluetooth devices. This is the same on the OS' settings app
  4. You're on charge of stopping/canceling the discovery of Bluetooth devices
 So, when it's all said and done, the app will display this:
We click on the "Scan for Bluetooth devices" and in this case I decided to start a new Activity to do this process.
Here's the code of this new "FoundBTDevices.java" class:
private void displayListOfFoundDevices()
{
    arrayOfFoundBTDevices = new ArrayList<BluetoothObject>();

    // start looking for bluetooth devices
    mBluetoothAdapter.startDiscovery();

    // Discover new devices
    // Create a BroadcastReceiver for ACTION_FOUND
    final BroadcastReceiver mReceiver = new BroadcastReceiver()
    {
        @Override
        public void onReceive(Context context, Intent intent)
        {
            String action = intent.getAction();
            // When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action))
            {
                // Get the bluetoothDevice object from the Intent
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                // Get the "RSSI" to get the signal strength as integer,
                // but should be displayed in "dBm" units
                int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI,Short.MIN_VALUE);

                // Create the device object and add it to the arrayList of devices
                BluetoothObject bluetoothObject = new BluetoothObject();
                bluetoothObject.setBluetooth_name(device.getName());
                bluetoothObject.setBluetooth_address(device.getAddress());
                bluetoothObject.setBluetooth_state(device.getBondState());
                bluetoothObject.setBluetooth_type(device.getType());    // requires API 18 or higher
                bluetoothObject.setBluetooth_uuids(device.getUuids());
                bluetoothObject.setBluetooth_rssi(rssi);

                arrayOfFoundBTDevices.add(bluetoothObject);

                // 1. Pass context and data to the custom adapter
                FoundBTDevicesAdapter adapter = new FoundBTDevicesAdapter(getApplicationContext(), arrayOfFoundBTDevices);

                // 2. setListAdapter
                setListAdapter(adapter);
            }
        }
    };
    // Register the BroadcastReceiver
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(mReceiver, filter);
}

This works by receiving data in a BroadcastReceiver, so let's break it down:
  1. Tell the BluetoothAdapter to start the discovery of new devices
  2. Create a Broadcast receiver and override the "onReceive" method
  3. The oddest part on this is the line of "BluetoothDevice.ACTION_FOUND.equals(Intent.getAction())" simply because this part you have to take it on faith as is "just do that and it'll work". Those kind of suck
  4. Just like in the Set of already paired devices, we need to get a BluetoothDevice object, and in this case we're getting it from the intent: "BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);"
  5. Create a new object, put it on an ArrayList and start the list from here by setting the list adapter to the adapter just created
  6. Finally, register the BroadcastReceiver
One thing you might notice different here is that I'm not returning an ArrayList and then passing that to the adapter.
The reason for this is because this is an Asynchronous task. This means the discovery of the devices might take all 12 seconds (or more) so returning an ArrayList immediately won't really work.
Also, by passing this data directly this way the list can be updated automatically with new devices coming in.
Alternately you could display a "wait while we look for devices" message and then return an ArrayList after X amount of seconds...it's up to you.

And finally, we're on charge of canceling the discovery. This doesn't get done automatically so if you start it and 2 seconds later you decide to close your app, the OS will continue searching for devices and eating up battery.
So:
@Override
protected void onPause()
{
    super.onPause();

    mBluetoothAdapter.cancelDiscovery();
}

I hope this has helped.
The source code for this project can be found here.

Eduardo.

16 comments:

  1. FoundBTDevicesAdapter adapter = new FoundBTDevicesAdapter(getApplicationContext(), arrayOfFoundBTDevices);

    is this a object of class ?
    and what are methods in it ?



    ReplyDelete
  2. Can we keep the process going on after discovery done!

    ReplyDelete
  3. nice tutorial. explanation is really good

    ReplyDelete
  4. I found your this post while searching for information about blog-related research ... It's a good post .. keep posting and updating information. best-bluetooth-transmitters

    ReplyDelete
  5. A biometric fingerprint scanner consists of a sensor that scans each finger before sending that image for entry into the computer or for comparison to a ready database. Since each human finger consists of ridges and valleys that make up a unique pattern specific only to that human being, there are no chances of any errors. barcode scanning devices

    ReplyDelete
  6. I followed your tutorial to a T and my application continually crashes while trying to scan for paired devices

    ReplyDelete
  7. Thanks for sharing the useful blog about the Source Code for Scanning the Bluetooth Devices in Android.

    Mobile Application Development Company in Coimbatore

    ReplyDelete
  8. Really I like your Blog! Thanks to Admin for Sharing the above information about Bluetooth Technology. I bookmarked this link for Feature reference. Keep sharing such good Articles. Addition to your Story here I am Contributing one more Similar Story What is the Need for a Bluetooth Device on Wheels?.

    ReplyDelete
  9. I would like to thank you for the efforts you have made in writing this article, Its good and Informative.
    android online training

    ReplyDelete
  10. why i don't get value of scan devices? they give me the error :- Activity has leaked IntentReceiver that was originally registered here. Are you missing a call to unregisterReceiver()?

    so plz tell me what i do?

    ReplyDelete
  11. Hi,
    I tested this tutorial everything works normally, activation of bluetooth, display of devices already connected, but with bluetooth scan nothing is displayed unfortunately, I do not know what is the cause. Please, please help me

    ReplyDelete
    Replies
    1. same here... cant scan for any bluetooth devices..

      Delete
  12. Hello,
    How to Scanning for Bluetooth devices when app is closed ? It must scan in background when app is closed by user.

    ReplyDelete
  13. The code doesnt show discoverable devices .it only display paired devices

    ReplyDelete