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.
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.