Android Beacon Detection Snippet
Guide to getting started with the AreaMetrics beacon monitoring platform.
Add Gradle dependencies
All of our dependencies are open-source, popular and well-maintained.
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.google.android.gms:play-services-base:12.0.1'
implementation 'org.altbeacon:android-beacon-library:2.16.3'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
}
Ensure Permissions are present in Manifest
The manifest permissions INTERNET and ACCESS_NETWORK_STATE should be added if they are not already present. If your app targets Android API 29+ (Android 10+), then the ACCESS_FINE_LOCATION and ACCESS_BACKGROUND_LOCATION permissions should also be added.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- The below is also necessary if targeting API 29+ -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
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.
package com.areametrics.nosdkandroid;
// AreaMetrics Snippet for Android v1.8
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.8";
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 = false;
// 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")) {
if (response.isSuccessful()) {
clearStoredBeacons();
updateStoredLastSend();
}
currentlySendingBatch = false;
}
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");
}
if (!publisherID.endsWith("80")) {
throw new IllegalArgumentException("Called startService with invalid Android Publisher ID");
}
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 : 5_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>());
int batchOverflow = batchStringSet.size() - 100;
if (batchOverflow > 0) {
int index = 0;
for (Iterator<String> i = batchStringSet.iterator(); i.hasNext();) {
i.next();
if (index <= batchOverflow) {
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 > 1800_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:
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 targets API 28 or below. If your app targets API 29+ (Android 10+), then both the ACCESS_FINE_LOCATION and ACCESS_BACKGROUND_LOCATION permissions are required. If your app does not already have code prompting the user to grant these permissions, you will need to implement the following:
Last updated