Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Support iBeacons In Your Native Android Mobile App

TwitterFacebookRedditLinkedInHacker News

I play around with iBeacons quite frequently. I created my own Internet of Things (IoT) iBeacon project as well as an AngularJS wrapper for using iBeacons in an Ionic Framework application. This time around I figured I’d take my iBeacon adventure to the next level and try to use them in a native Android mobile application.

Using the AltBeacon library by Radius Networks we can easily add iBeacon monitoring and ranging support to our native Android application. We’re going to see how to scan for a variety of proximity beacons and display them within an application.

In our example we will be scanning for beacons and adding them to a list in the Android UI. I am personally using a variety of iBeacons to test with including those from Gimbal, Estimote, and Radius Networks.

Let’s start by creating a fresh Android project. You can do this with Android Studio, or you can do this from the Terminal (Mac and Linux) or Command Prompt (Windows):

android create project --activity Main --package com.nraboy.beaconproject --target 12 --path . --gradle --gradle-version 2.10

I’m personally using Android Studio, but both solutions should work without issue. For more information on using the command line, see a previous article I wrote on the topic.

The first thing we want to do is include the AltBeacon library in our Gradle build process. Find the project’s build.gradle file and add the following line to the dependencies section:

compile 'org.altbeacon:android-beacon-library:2+'

Now we’ll be able to use the library in our project.

The AltBeacon library requires certain application permissions be set. For example, iBeacons are Bluetooth devices so we must enable various Bluetooth permissions. Since they work off proximity, we’ll also need various location services enabled. All of this can be done in the app/src/main/AndroidManifest.xml file. Open this file and include the following lines:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

These permissions should be added before the application tag, but after the manifest tag.

The beacons that we discover from this application will be placed into a list. This means we need to alter the layout to our activity to include an Android ListView component. Open the project’s app/src/main/res/layout/activity_main.xml and include the following XML markup:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.nraboy.beaconproject.MainActivity">

    <ListView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/listView"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

Much of the above is pretty standard Android initial layout code. We just included a ListView component in it.

Now we can focus on all the code involved in scanning for iBeacons and populating the list that we just created in the UI. Open the project’s app/src/main/java/com/nraboy/beaconproject/MainActivity.java and include the following code:

package com.nraboy.beaconproject;

import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.RemoteException;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import org.altbeacon.beacon.*;
import java.util.*;

public class MainActivity extends AppCompatActivity implements BeaconConsumer {

    private static final String TAG = "BEACON_PROJECT";
    private ArrayList<String> beaconList;
    private ListView beaconListView;
    private ArrayAdapter<String> adapter;
    private BeaconManager beaconManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.beaconList = new ArrayList<String>();
        this.beaconListView = (ListView) findViewById(R.id.listView);
        this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, this.beaconList);
        this.beaconListView.setAdapter(adapter);
        this.beaconManager = BeaconManager.getInstanceForApplication(this);
        this.beaconManager.getBeaconParsers().add(new BeaconParser(). setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
        this.beaconManager.bind(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle("This app needs location access");
                builder.setMessage("Please grant location access so this app can detect beacons");
                builder.setPositiveButton(android.R.string.ok, null);
                builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
                    }
                });
                builder.show();
            }
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        this.beaconManager.unbind(this);
    }

    @Override
    public void onBeaconServiceConnect() {
        this.beaconManager.setRangeNotifier(new RangeNotifier() {
            @Override
            public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
                if (beacons.size() > 0) {
                    beaconList.clear();
                    for(Iterator<Beacon> iterator = beacons.iterator(); iterator.hasNext();) {
                        beaconList.add(iterator.next().getId1().toString());
                    }
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            adapter.notifyDataSetChanged();
                        }
                    });
                }
            }
        });
        try {
            this.beaconManager.startRangingBeaconsInRegion(new Region("MyRegionId", null, null, null));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

}

The above code is just one of two things we’re going to look at. Let’s break it down because it is rather long and complicated.

In the activities onCreate method we configure our list to accept data from an ArrayList<String>. All bootstrapping of our list can be seen below:

this.beaconList = new ArrayList<String>();
this.beaconListView = (ListView) findViewById(R.id.listView);
this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, this.beaconList);
this.beaconListView.setAdapter(adapter);

What is really important to us is the initialization of our iBeacon code. Our activity implements the BeaconConsumer which is valuable during the setup code here:

this.beaconManager = BeaconManager.getInstanceForApplication(this);
this.beaconManager.getBeaconParsers().add(new BeaconParser(). setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
this.beaconManager.bind(this);

In the above code we set up a parser that will look for Bluetooth packets that match the layout. Not all Bluetooth devices are iBeacons, so we are specifying to only scan for hardware that meets the specification.

Let’s skip ahead to the onBeaconServiceConnect method. Here we’ll define a listener for beacons that are in range and define which beacons to listen for.

@Override
public void onBeaconServiceConnect() {
    this.beaconManager.setRangeNotifier(new RangeNotifier() {
        @Override
        public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
            if (beacons.size() > 0) {
                beaconList.clear();
                for(Iterator<Beacon> iterator = beacons.iterator(); iterator.hasNext();) {
                    beaconList.add(iterator.next().getId1().toString());
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        adapter.notifyDataSetChanged();
                    }
                });
            }
        }
    });
    try {
        this.beaconManager.startRangingBeaconsInRegion(new Region("MyRegionId", null, null, null));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

For the above to make sense, let’s figure out what beacons we’re going to listen for. You see that we are defining a single region with only a region id. The beacon id, major, and minor codes are left as null. This means we want all beacons that match the iBeacon specification to be listened for.

Jumping back to the didRangeBeaconsInRegion method, we can receive any number of iBeacons in a single notification. We choose to iterate over each beacon returned and add them to the list.

So far so good right?

Well, starting in Android 5.0, users need to grant permission to use various device features within the application. If permission is not requested, the functionality will not work.

Jumping back to the onCreate method you’ll notice:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("This app needs location access");
        builder.setMessage("Please grant location access so this app can detect beacons");
        builder.setPositiveButton(android.R.string.ok, null);
        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
            }
        });
        builder.show();
    }
}

The above code is how we request permission for certain device features. Of course we don’t really need to show an alert, but it is a good idea to explain why you are going to ask for permission. Once permission is granted, Bluetooth and location services can be used.

Now in most scenarios we won’t want to scan for any and every iBeacon that exists. We probably want to set specific iBeacons based on maybe a remote database. Let’s modify the onBeaconServiceConnect method a bit:

@Override
public void onBeaconServiceConnect() {
    this.beaconManager.setRangeNotifier(new RangeNotifier() {
        @Override
        public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
            if (beacons.size() > 0) {
                for(Iterator<Beacon> iterator = beacons.iterator(); iterator.hasNext();) {
                    Beacon beacon = iterator.next();
                    if(!beaconList.contains(beacon.getId1().toString())) {
                        beaconList.add(beacon.getId1().toString());
                    }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        adapter.notifyDataSetChanged();
                    }
                });
            }
        }
    });
    try {
        this.beaconManager.startRangingBeaconsInRegion(new Region("gimbal", Identifier.parse("9A4D89AE-EC35-4191-AC68-888D132FB786"), null, null));
        this.beaconManager.startRangingBeaconsInRegion(new Region("radnetworks", Identifier.parse("2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6"), null, null));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

I’ve added two beacon regions specific to two of my iBeacons. Although we are scanning for all iBeacons, we are only listening for those two regions now. The didRangeBeaconsInRegion method changed a bit too. Although we can potential listen for multiple iBeacons per region, in my case I only have one per each. Instead of refreshing the list every time, we are just adding them if they don’t already exist.

The application should be runnable at this point.

Conclusion

We just saw how to use the AltBeacon library in our native Android application to scan for iBeacons and set up regions. There are plenty of other features as part of the AltBeacon library, such as monitoring, but that is best left for your imagination.

The iBeacons I used to test this code were from Gimbal, Estimote, and Radius Networks, but any Bluetooth device that follows the iBeacon specification should work.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.