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'}
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.
Please ensure that you edit the package declaration at the top accordingly.
AreaMetrics.javapackage 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);​@Overridepublic 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());}​@Overridepublic 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 monitoringbeaconManager = 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")); //ibeaconbeaconManager.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> {@Overrideprotected 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;}@Overrideprotected 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​@Overridepublic 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");}}}​@Overridepublic 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());}}​@Overridepublic void didDetermineStateForRegion(int state, Region region) {sendBeaconBatchIfNeeded();}​@Overridepublic void onBeaconServiceConnect() {beaconManager.addRangeNotifier(new RangeNotifier() {@Overridepublic 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";}}}
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.javaimport android.app.Application;public class MyApp extends Application {@Overridepublic 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!"
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: