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.

build.gradle (Module: app)
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.

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

AreaMetrics.java
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:

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