Android Beacon Detection Snippet

Last updated 3 months ago

Guide to getting started with the AreaMetrics beacon monitoring platform.

Add Gradle dependencies

All of our dependencies are open-source, popular and well-maintained.

build.gradle (Module: app)
dependencies {
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.google.android.gms:play-services-base:12.0.1'
implementation 'org.altbeacon:android-beacon-library:2.15.1'
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
}

Ensure Permissions are present in Manifest

The manifest permissions INTERNET and ACCESS_NETWORK_STATE should be added if they are not already present.

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

The other permissions are merged into your Manifest automatically by our open-source dependencies. These permissions are BLUETOOTH, BLUETOOTH_ADMIN, RECEIVE_BOOT_COMPLETED, and ACCESS_COARSE_LOCATION.

Add the AreaMetrics.java file to your project

Please ensure that you edit the package declaration at the top accordingly.

AreaMetrics.java
package com.areametrics.nosdkandroid;
// AreaMetrics Snippet for Android v1.5
import android.Manifest;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.RemoteException;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconConsumer;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.RangeNotifier;
import org.altbeacon.beacon.Region;
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.logging.Loggers;
import org.altbeacon.beacon.powersave.BackgroundPowerSaver;
import org.altbeacon.beacon.startup.BootstrapNotifier;
import org.altbeacon.beacon.startup.RegionBootstrap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.QueryMap;
public enum AreaMetrics implements BootstrapNotifier, BeaconConsumer, Callback<ResponseBody> {
INSTANCE;
private static final String TAG = "AMS";
private static final String SNIPPET_VERSION = "1.5";
private static String AM_SNIPPET_STORE = "AM_SNIPPET_STORE";
private static String AM_BEACON_BATCH = "AM_BEACON_BATCH";
private static String AM_BATCH_LAST_SENT = "AM_BATCH_LAST_SENT";
private String pubID = null;
private Context myContext = null;
private String adId = null;
private String vendorID = null;
private RegionBootstrap regionBootstrap;
private BackgroundPowerSaver backgroundPowerSaver;
private BeaconManager beaconManager;
private Set<String> rangingRegions = new HashSet<>();
private Set<String> amUUIDs = new HashSet<>(Arrays.asList("B9407F30-F5F8-466E-AFF9-25556B577272"));
private Map<String, Long> lastSentBeacons = new HashMap<>();
private boolean currentlySendingBatch;
// Network API Declaration
public interface AMAPIService {
@GET("user_init")
Call<ResponseBody> userInit(@QueryMap Map<String, String> params);
@FormUrlEncoded
@POST("beacon_batch")
Call<ResponseBody> beaconBatch(@FieldMap Map<String, String> params);
}
private Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.areametrics.com/v3/").build();
private AMAPIService api = retrofit.create(AMAPIService.class);
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
String url = call.request().url().toString();
if (url.endsWith("beacon_batch")) {
currentlySendingBatch = false;
updateStoredLastSend();
clearStoredBeacons();
}
if (BuildConfig.DEBUG) Log.d(TAG, "Response: " + response.message());
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
String url = call.request().url().toString();
if (url.endsWith("beacon_batch")) {
currentlySendingBatch = false;
}
if (BuildConfig.DEBUG) Log.d(TAG, "Failure: " + t.getMessage());
}
// Start Service
public void startService(Application application, String publisherID) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
if (BuildConfig.DEBUG) Log.d(TAG, "Early return out of startService due to running KitKat");
return;
}
if (application == null || publisherID == null) {
throw new IllegalArgumentException("Called startService without Publisher ID or Application context");
}
pubID = publisherID;
myContext = application;
new FetchAdID().execute();
if (isUserGDPR()) {
return;
}
LogManager.setLogger(Loggers.empty());
LogManager.setVerboseLoggingEnabled(false);
//beacon monitoring
beaconManager = BeaconManager.getInstanceForApplication(myContext);
beaconManager.getBeaconParsers().add(new BeaconParser("ibeacon").setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24")); //ibeacon
beaconManager.setRegionStatePersistenceEnabled(false);
Region region = new Region("AnyBeacon", null, null, null);
regionBootstrap = new RegionBootstrap(this, region);
backgroundPowerSaver = new BackgroundPowerSaver(myContext);
beaconManager.bind(this);
beaconManager.setForegroundScanPeriod(1_100L);
beaconManager.setForegroundBetweenScanPeriod(5_000L);
beaconManager.setBackgroundScanPeriod(8_000L);
beaconManager.setBackgroundBetweenScanPeriod(300_000L);
try {
beaconManager.updateScanPeriods();
} catch (RemoteException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "RemoteException in updateScanPeriods()");
}
sendBeaconBatchIfNeeded();
if (BuildConfig.DEBUG) Log.d(TAG, "AreaMetrics Initialized Successfully!");
}
// AdID Fetch
private class FetchAdID extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
String gid = null;
if (isGooglePlayServicesAvailable(myContext)) {
try {
AdvertisingIdClient.Info adInfo = AdvertisingIdClient.getAdvertisingIdInfo(myContext);
if (adInfo != null && adInfo.getId() != null) {
gid = adInfo.getId();
}
} catch (Exception e) {
if (BuildConfig.DEBUG) Log.d(TAG, "Getting Ad Client Failed: " + e.getMessage());
}
}
return gid;
}
@Override
protected void onPostExecute (String s) {
if (s != null) {
adId = s;
}
sendUserInit();
}
}
public final boolean isGooglePlayServicesAvailable(Context context) {
GoogleApiAvailability availability = GoogleApiAvailability.getInstance();
if (availability != null) {
int resultCode = availability.isGooglePlayServicesAvailable(context);
return resultCode == ConnectionResult.SUCCESS;
} else {
return false;
}
}
// Beacon Monitoring
@Override
public void didEnterRegion(Region region) {
sendBeaconBatchIfNeeded();
if (!rangingRegions.contains(region.getUniqueId())) {
rangingRegions.add(region.getUniqueId());
try {
beaconManager.startRangingBeaconsInRegion(region);
} catch (RemoteException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "startRangingBeaconsInRegion failed due to unbound beacon manager");
}
}
}
@Override
public void didExitRegion(Region region) {
sendBeaconBatchIfNeeded();
try {
beaconManager.stopRangingBeaconsInRegion(region);
} catch (RemoteException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "stopRangingBeaconsInRegion failed due to unbound beacon manager");
}
if (region.getUniqueId() != null) {
rangingRegions.remove(region.getUniqueId());
}
}
@Override
public void didDetermineStateForRegion(int state, Region region) {
sendBeaconBatchIfNeeded();
}
@Override
public void onBeaconServiceConnect() {
beaconManager.addRangeNotifier(new RangeNotifier() {
@Override
public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
for (Beacon beacon : beacons) {
if (beacon.getIdentifiers().size() < 3) {
continue;
}
storeBeaconIfNeeded(beacon);
}
}
});
try {
beaconManager.startRangingBeaconsInRegion(new Region("AnyBeacon", null, null, null));
} catch (RemoteException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "Failed to start ranging beacons");
}
}
public Context getApplicationContext() {
return myContext;
}
public void unbindService(ServiceConnection var1) {
getApplicationContext().unbindService(var1);
}
public boolean bindService(Intent var1, ServiceConnection var2, int var3) {
return getApplicationContext().bindService(var1, var2, var3);
}
private void clearStoredBeacons() {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet(AM_BEACON_BATCH, new HashSet<String>());
editor.apply();
}
private void storeBeaconIfNeeded(Beacon beacon) {
String beaconID = String.format(Locale.US, "%s.%s.%s",
beacon.getId1().toString(),
beacon.getId2().toString(),
beacon.getId3().toString());
if (lastSentBeacons.containsKey(beaconID)) {
long lastTime = lastSentBeacons.get(beaconID);
long delay = amUUIDs.contains(beacon.getId1().toString()) ? 5_000 : 15_000;
if (System.currentTimeMillis() - lastTime < delay) {
return; // early return to limit excess
}
}
lastSentBeacons.put(beaconID, System.currentTimeMillis());
try {
JSONObject beaconJSON = new JSONObject();
long epochSeconds = System.currentTimeMillis() / 1000L;
beaconJSON.put("time", epochSeconds);
beaconJSON.put("uuid", beacon.getId1().toString());
beaconJSON.put("major", beacon.getId2().toInt());
beaconJSON.put("minor", beacon.getId3().toInt());
beaconJSON.put("rssi", beacon.getRssi());
beaconJSON.put("tx_power", beacon.getTxPower());
beaconJSON.put("avg_rssi", beacon.getRunningAverageRssi());
beaconJSON.put("btype", beacon.getParserIdentifier());
beaconJSON.put("manufacturer", beacon.getManufacturer());
beaconJSON.put("beacon_accuracy", beacon.getDistance());
beaconJSON.put("address", beacon.getBluetoothAddress());
beaconJSON.put("num_rssi", beacon.getMeasurementCount());
beaconJSON.put("name", beacon.getBluetoothName());
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
Set<String> batchStringSet = prefs.getStringSet(AM_BEACON_BATCH, new HashSet<String>());
if (batchStringSet.size() >= 400) {
int index = 0;
for (Iterator<String> i = batchStringSet.iterator(); i.hasNext();) {
i.next();
if (index < 10) {
i.remove();
} else {
break;
}
index++;
}
}
batchStringSet.add(beaconJSON.toString());
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet(AM_BEACON_BATCH, batchStringSet);
editor.apply();
} catch (JSONException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "JSONException while storing Beacon in batch!");
}
sendBeaconBatchIfNeeded();
}
private void updateStoredLastSend() {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(AM_BATCH_LAST_SENT, System.currentTimeMillis());
editor.apply();
}
private void sendBeaconBatchIfNeeded() {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
Set<String> batchStringSet = prefs.getStringSet(AM_BEACON_BATCH, new HashSet<String>());
if (batchStringSet.size() > 0) {
long batchLastSend = prefs.getLong(AM_BATCH_LAST_SENT, 0);
if (System.currentTimeMillis() - batchLastSend > 3600_000) {
sendBeaconBatchToServer(batchStringSet);
}
}
}
private void sendBeaconBatchToServer(Set<String> batchStringSet) {
if (currentlySendingBatch) {
return;
}
currentlySendingBatch = true;
Map<String, String> params = new HashMap<>();
params.put("pub_id", pubID);
params.put("os", "android");
params.put("os_ver", Build.VERSION.RELEASE);
params.put("model", Build.MANUFACTURER + " " + Build.MODEL);
params.put("user_locale", Locale.getDefault().getCountry());
params.put("snippet_ver", SNIPPET_VERSION);
if (!isUserGDPR()) {
if (getVendorID() != null) {
params.put("vendor_id", getVendorID());
}
if (adId != null) {
params.put("ad_id", adId);
}
params.put("agent", System.getProperty("http.agent"));
}
try {
JSONArray batchArray = new JSONArray();
for (String jsonStr : batchStringSet) {
batchArray.put(new JSONObject(jsonStr));
}
params.put("batch", batchArray.toString());
Call<ResponseBody> call = api.beaconBatch(params);
call.enqueue(this);
} catch (JSONException e) {
clearStoredBeacons();
currentlySendingBatch = false;
if (BuildConfig.DEBUG) Log.d(TAG, "JSONException while assembling Beacon Batch for sending!");
}
}
private void sendUserInit() {
Map<String, String> params = new HashMap<>();
params.put("pub_id", pubID);
params.put("os", "android");
params.put("os_ver", Build.VERSION.RELEASE);
params.put("model", Build.MANUFACTURER + " " + Build.MODEL);
params.put("locale", Locale.getDefault().getCountry());
params.put("snippet_ver", SNIPPET_VERSION);
if (getVendorID() != null) {
params.put("vendor_id", getVendorID());
}
if (!isUserGDPR() && adId != null) {
params.put("ad_id", adId);
}
params.put("loc_permission", getLocPermission());
Call<ResponseBody> call = api.userInit(params);
call.enqueue(this);
}
boolean isUserGDPR() {
Set<String> countries = getGDPRCountries();
String device = Locale.getDefault().getCountry();
return device != null && countries.contains(device);
}
private Set<String> getGDPRCountries() {
return new HashSet<>(Arrays.asList("AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "IS", "LI", "NO"));
}
private String getVendorID() {
if (vendorID == null) {
try {
if (myContext != null && myContext.getContentResolver() != null) {
String androidId = Settings.Secure.getString(myContext.getContentResolver(), Settings.Secure.ANDROID_ID);
if (androidId != null) {
vendorID = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")).toString();
}
}
} catch (UnsupportedEncodingException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "VendorID encoding error");
}
}
return vendorID;
}
private String getLocPermission() {
int fine = ActivityCompat.checkSelfPermission(myContext, Manifest.permission.ACCESS_FINE_LOCATION);
int coarse = ActivityCompat.checkSelfPermission(myContext, Manifest.permission.ACCESS_COARSE_LOCATION);
if (fine == PackageManager.PERMISSION_GRANTED || coarse == PackageManager.PERMISSION_GRANTED) {
return "authorized";
} else {
return "denied";
}
}
}

Implement the call to startService

Call startService on AreaMetrics in the onCreate method of your Application subclass (create one if you don't have one already). Replace "PUBLISHER_ID" in the following example with your publisher ID:

ApplicationSubclass.java
import android.app.Application;
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
AreaMetrics.INSTANCE.startService(this, "PUBLISHER_ID");
}
}

If you use Crashlytics, please ensure that the startService call happens after your Crashlytics initialization.

When launching your app, you should see printed in the debug console, "AreaMetrics Initialized Successfully!"

Ask the user for Location Permission

AreaMetrics Snippet requires ACCESS_COARSE_LOCATION permission. If your app does not already have code requesting user permission for ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION (FINE automatically grants COARSE), you will need to implement the following: