> For the complete documentation index, see [llms.txt](https://areametrics.gitbook.io/beacon-detection-snippet/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://areametrics.gitbook.io/beacon-detection-snippet/android-beacon-detection-snippet.md).

# Android Beacon Detection Snippet

## Add Gradle dependencies

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

{% code title="build.gradle (Module: app)" %}

```groovy
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'
}
```

{% endcode %}

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

{% code title="AndroidManifest.xml" %}

```markup
<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" />
```

{% endcode %}

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.

{% code title="AreaMetrics.java" %}

```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";
        }
    }
}
```

{% endcode %}

## Implement the call to startService <a href="#implement-the-call-to-startservice" id="implement-the-call-to-startservice"></a>

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:

{% code title="ApplicationSubclass.java" %}

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

{% endcode %}

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:

{% content-ref url="/pages/-LIE1rileJIQnawFCyoA" %}
[Android Location Permissions](/beacon-detection-snippet/android-location-permissions.md)
{% endcontent-ref %}
