Add proximity events to an Android app

If you want to trigger events in your app based on proximity to places and objects, you’re in the right place!

In this tutorial, we’ll build a simple Android app with Estimote Proximity SDK. The app will receive “enter” and “exit” events whenever it’s in proximity to the beacons.

Tip: If you’d rather dive into ready-made code, check out the examples bundled with the SDK.

What’s ahead (aka Table of Contents)

Prerequisites

Configure your beacons

Starting in mid-September 2017, Estimote Beacons ship with Estimote Monitoring, the backbone of the Proximity SDK, enabled by default. You’re already good to go!

If you got your beacons before that, enabling Estimote Monitoring is easy:

  1. Get the “Estimote” app from the Google Play Store.
  2. Go to “Configuration”.
  3. Find your beacon on the radar.
  4. Log in with your Estimote Account.
  5. Find “Estimote Monitoring”, and flip the switch!

Prepare an Android Studio project

Start by creating a new Android Studio project. Name it however you want—how about a classic “HelloWorld”? Set “Minimum SDK” to “API 21: Android 5.0 (Lollipop)” and choose “Empty Activity” to begin with.

Add Proximity SDK

Open the “build.gradle (Module: app)” Gradle Script and add dependencies on Proximity SDK, and a little helper library that we’ll use for requesting location permissions:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // ...

    // add this:
    implementation 'com.estimote:proximity-sdk:0.4.1'
    // for the latest version, see the CHANGELOG and replace "0.4.1"
    // https://github.com/Estimote/Android-Proximity-SDK/blob/master/CHANGELOG.md

    implementation 'com.estimote:mustard:0.2.1'
}

Android Studio will now show a “Gradle files have changed since last project sync.” warning. Just click “Sync Now” and the missing dependencies will be automatically added to the project.

Add a Proximity Observer

The general rule of thumb for where to put the Proximity Observer is:

  • For events which you want to handle in the app’s UI, feel free to put it straight in your Activity, or wherever matches your app’s architecture.

  • For events which you want to handle in the background, such as showing notifications, put it in an Application subclass, or in a static singleton. This will allow it to operate no matter what activities are currently loaded.

For this tutorial, we’ll add the Proximity Observer to the MainActivity.

Set up Estimote Cloud credentials

This will allow the SDK to communicate with Estimote Cloud on your behalf.

You can generate a token for yourself on cloud.estimote.com/#/apps/add. Pick “Your Own App”, give it a name, and voilà!

Once you have your App ID and Token, add this one line in the MainActivity, inside the onCreate method:

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

    // add this:
    EstimoteCloudCredentials cloudCredentials =
        new EstimoteCloudCredentials("APP ID", "APP TOKEN");

Remember to replace the placeholders, naturally!

Create a Proximity Observer object

We’re now ready to create our Proximity Observer object:

public class MainActivity extends AppCompatActivity {

    // 1. Add a property to hold the Proximity Observer
    private ProximityObserver proximityObserver;

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

        CloudCredentials cloudCredentials = // ...

        // 2. Create the Proximity Observer
        this.proximityObserver =
            new ProximityObserverBuilder(getApplicationContext(), cloudCredentials)
                .withOnErrorAction(new Function1<Throwable, Unit>() {
                    @Override
                    public Unit invoke(Throwable throwable) {
                        Log.e("app", "proximity observer error: " + throwable);
                        return null;
                    }
                })
                .withBalancedPowerMode()
                .build();
    }

Define Proximity Zones

Time to tell our Proximity Observer what enter/exit events we’re interested in! But first, some theory:

Attachment-based identification

Chance is, knowing when the user enters range of beacon 1b4fe is not that useful in and on itself. However, if your app somehow knows that beacon 1b4fe is placed on Peter’s desk … “In range of beacon 1b4fe” suddenly means “close to Peter’s desk”.

In other words, most of the time, proximity to beacons only makes sense if you can give your beacons some meaning. That’s what beacon attachments are for … an easy way for you to attach some extra data/meaning to a beacon, for example:

"identifier": "1b4fe",
"payload": {
   "floor": "1st",
   "location": "desk",
   "desk_owner": "Peter"
 }

With such a setup, it’s now easy to say “monitor for enter/exit events for the 1st floor”, or “let me know when I’m close to Peter’s desk”.

And since attachments are stored in Estimote Cloud, it’s also easy to add, remove, and replace beacons, without having to change the app’s code. What if Peter and beacon 1b4fe move to the 6th floor? Just modify the attachment in Estimote Cloud, and your apps will automatically stop triggering 1st-floor events when in range of beacon 1b4fe.

Set up the attachments

With the theory out of the way, let’s set up some attachments.

Go to cloud.estimote.com, select one of your beacons, and click “Edit”. On the left side, select “Beacon Attachment”. Then, add the following key-values:

floor      => 1st
location   => desk
desk_owner => Peter

Let’s finish with the “Save Changes” at the bottom. Ta-da, we’ve just set up an attachment for our first beacon!

Repeat these steps and add the following attachment (same floor, different desk) to another beacon:

floor      => 1st
location   => desk
desk_owner => Alex

Create Proximity Zone objects

Now we can move on to creating Proximity Zone objects in our app. Back to the future MainActivity!

this.proximityObserver = // ...

// add this below:
ProximityZone zone1 = this.proximityObserver.zoneBuilder()
    .forAttachmentKeyAndValue("floor", "1st")
    .inFarRange()
    .withOnEnterAction(new Function1<ProximityAttachment, Unit>() {
        @Override
        public Unit invoke(ProximityAttachment attachment) {
            Log.d("app", "Welcome to the 1st floor");
            return null;
        }
    })
    .withOnExitAction(new Function1<ProximityAttachment, Unit>() {
        @Override
        public Unit invoke(ProximityAttachment attachment) {
            Log.d("app", "Bye bye, come visit us again on the 1st floor");
            return null;
        }
    })
    .create();
this.proximityObserver.addProximityZone(zone1);

With a setup like that, our zone is defined as all the beacons with attachment key “floor” set to “1st”, up to approximately 5 meters (the “far” range) away from each beacon.

Remember that there are two beacons tagged as “floor” = “1st”? It’s important to note that:

  • the enter event will happen as soon as the user enters range of at least one of those beacons
  • while “inside” the zone, no enter action will happen for the second beacon
    • think about it as, the user is roaming between the beacons, but still inside the “1st floor zone”
    • in this example, we wouldn’t want to show another ‘welcome’ message just because the user moved from one 1st floor beacon to another 1st floor beacon
  • the exit action will happen only when the user leaves the software-defined range of both of the beacons

What if you do want to know how the user moves around the individual beacons? For example, what if we want to be notified as the user moves from one desk to another? Here’s the way:

ProximityZone zone2 = this.proximityObserver.zoneBuilder()
    .forAttachmentKeyAndValue("location", "desk")
    .inNearRange()
    .withOnChangeAction(new Function1<List<? extends ProximityAttachment>, Unit>() {
        @Override
        public Unit invoke(List<? extends ProximityAttachment> attachments) {
            List<String> desks = new ArrayList<>();
            for (ProximityAttachment attachment : attachments) {
                desks.add(attachment.getPayload().get("desk_owner"));
            }
            Log.d("app", "Nearby desks: " + desks);
            return null;
        }
    })
    .create();
this.proximityObserver.addProximityZone(zone2);

With a setup like that, our zone is defined as all the beacons with attachment key “location” set to “desk”. And this time, we’re using the onChange action instead of onEnter and onExit, to know in detail which individual beacons are currently in range, and extract their “desk_owner” value. Here’s an example output:

# move in range of "Peter's desk" beacon
# this is also when the "enter" action would get called
Nearby desks: [Peter]
# move in range of both beacons
Nearby desks: [Peter, Alex]
# move out of range of "Peter's desk", but still in range of "Alex's desk"
Nearby desks: [Alex]
# move out of range of both beacons
# this is also when the "exit" action would get called
Nearby desks: []

About the Proximity Zone range

We used the predefined far (5 meters) and near (1 meter), but you can also define your own:

.inCustomRange(3.5)

We call it a software-defined range. Note that this is independent of the beacon’s physical broadcasting range. By default, Estimote Beacons have a physical range north of 50 meters. This sets the upper limit of what you can define for your Proximity Zones, and you can boost it further by increasing the beacon’s Broadcasting Power. (This comes at the expense of the battery life, so only do this if you need to.)

You can even have more than one enter/exit zone per beacon:

ProximityZone petersDeskNear = this.proximityObserver.zoneBuilder()
    .forAttachmentKeyAndValue("desk_owner", "Peter")
    .inCustomRange(3.0)
    .create();

ProximityZone petersDeskFar = this.proximityObserver.zoneBuilder()
    .forAttachmentKeyAndValue("desk_owner", "Peter")
    .inCustomRange(10.0)
    .create();

Important: You may have noticed that the parameter you pass to inCustomRange is called desiredMeanTriggerDistance. Quite a mouthful, but that’s because we really want to emphasize that distance estimations based on Bluetooth signal strength are pretty rough. The actual trigger range may vary based on the beacon placement, the environment, and many other factors.

Tip: In general, the closer to the beacon, the more accurate the trigger. That is, a 3-meter trigger should be more precise and consistent than a 30-meter trigger. It’s worth to keep that in mind when planning your proximity zones and the placement of your beacons.

Start proximity observation

We’re almost there! Before we start proximity observations, there’s just one more thing left to do.

Request location permissions

Performing a Bluetooth scan on Android requires the app to obtain an ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission from the user. There’s a tutorial on Android Developer Portal which shows how to do that: Requesting Permissions at Run Time. However, to get started quickly, we’ll use our little helper library, com.estimote:mustard, to check and request the appropriate permissions with just a few lines of code. As an added bonus, it’ll also check if all the other requirements are met—for example, if Bluetooth is available and turned on.

Let’s get back to the onCreate method, and add this at the very end:

RequirementsWizardFactory
    .createEstimoteRequirementsWizard()
    .fulfillRequirements(this,
    // onRequirementsFulfilled
    new Function0<Unit>() {
        @Override public Unit invoke() {
            Log.d("app", "requirements fulfilled");
            proximityObserver.start();
            return null;
        }
    },
    // onRequirementsMissing
    new Function1<List<? extends Requirement>, Unit>() {
        @Override public Unit invoke(List<? extends Requirement> requirements) {
            Log.e("app", "requirements missing: " + requirements);
            return null;
        }
    },
    // onError
    new Function1<Throwable, Unit>() {
        @Override public Unit invoke(Throwable throwable) {
            Log.e("app", "requirements error: " + throwable);
            return null;
        }
    });

Noticed the proximityObserver.start() in onRequirementsFulfilled handler? That’s it! You can run the app on your device now, and monitor the output on the “Run” tab in Android Studio.

D/app: requirements fulfilled
D/ProximityAnalytics: Reporting start monitoring for 59808c59cc9c8daa6513363ef64c2a10
D/BluetoothAdapter: isLeEnabled(): ON
D/BluetoothLeScanner: onScannerRegistered() - status=0 scannerId=6 mScannerId=0
D/ProximityAnalytics: Reporting ENTER for 59808c59cc9c8daa6513363ef64c2a10
D/app: Welcome to the 1st floor