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:1.0.3'
    // for the latest version, see the CHANGELOG and replace "1.0.3"
    // 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)
                .onError(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:

Tags and attachments

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 tags and attachments are for … an easy way for you to attach some extra data/meaning to a beacon, for example:

"identifier": "1b4fe",
"tag": "desks",
"attachments": {
   "desk-owner": "Peter"
 }

With such a setup, it’s now easy to say “monitor proximity to desks”, and also figure out whose desk we’re close to.

And since tags and attachments are stored in Estimote Cloud, it’s also easy to change the setup without having to change the app’s code. For example, if sombebody else takes Peter’s desk, just change the “desk-owner” to a new one. If your app was coded to welcome the owner at their desk by their name, it’ll now start using the new one.

Set up tags and attachments

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

Go to cloud.estimote.com, select one of your beacons, and click “Edit”. Then:

  • to set up a tag: Click on “Tags”, “Create New Tag”, enter “desks”, and click “Add”.
  • to set up attachments: On the left side, select “Beacon Attachment”. Then, add a “desk-owner” key with value set to “Peter”.

Finish with the “Save Changes” at the bottom. Ta-da, we’ve just set up our first beacon!

Repeat these steps for another beacon. Use the same “desks” tag, but this time, set the “desk-owner” to “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 zone = new ProximityZoneBuilder()
    .forTag("desks")
    .inNearRange()
    .onEnter(new Function1<ProximityZoneContext, Unit>() {
        @Override
        public Unit invoke(ProximityZoneContext context) {
            String deskOwner = context.getAttachments().get("desk-owner");
            Log.d("app", "Welcome to " + deskOwner + "'s desk");
            return null;
        }
    })
    .onExit(new Function1<ProximityZoneContext, Unit>() {
        @Override
        public Unit invoke(ProximityZoneContext context) {
            Log.d("app", "Bye bye, come again!");
            return null;
        }
    })
    .build();

Note that for this setup to work, you need to spread the beacons apart a good few meters. If they overlap, then moving from one beacon to the other is considered moving within the zone, and it won’t trigger additional enter/exit actions.

If you want to know about movements inside a zone spanned by multiple overlapping beacons, you can use the “onContextChange” action instead. Think about it as: I’m still in the same desks zone (hence no new enter/exits), but my context (which specific desks are in range) has changed (hence the “onContextChange” action).

ProximityZone zone = new ProximityZoneBuilder()
    // ...
    .onContextChange(new Function1<Set<? extends ProximityZoneContext>, Unit>() {
        @Override
        public Unit invoke(Set<? extends ProximityZoneContext> contexts) {
            List<String> deskOwners = new ArrayList<>();
            for (ProximityZoneContext context : contexts) {
                deskOwners.add(context.getAttachment().get("desk-owner"));
            }
            Log.d("app", "In range of desks: " + deskOwners);
            return null;
        }
    })
    // ...

Here’s how this would work in an overlapping scenario:

# 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

There are predefined far (5 meters) and near (1 meter), but you can also define your own:

ProximityZone zone = new ProximityZoneBuilder()
    // ...
    .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 innerZone = new ProximityZoneBuilder()
    .forTag("treasure")
    .inCustomRange(3.0)
    .create();

ProximityZone outerZone = new ProximityZoneBuilder()
    .forTag("treasure")
    .inCustomRange(9.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.startObserving(zone);
            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.startObserving(zone) 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 Peter's desk