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)
1
dependencies {
2
implementation 'com.android.support:appcompat-v7:28.0.0'
3
implementation 'com.google.android.gms:play-services-base:12.0.1'
4
implementation 'org.altbeacon:android-beacon-library:2.16.3'
5
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
6
}
Copied!

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
1
<uses-permission android:name="android.permission.INTERNET" />
2
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3
<!-- The below is also necessary if targeting API 29+ -->
4
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
5
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Copied!
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
1
package com.areametrics.nosdkandroid;
2
3
// AreaMetrics Snippet for Android v1.8
4
5
import android.Manifest;
6
import android.app.Application;
7
import android.content.Context;
8
import android.content.Intent;
9
import android.content.ServiceConnection;
10
import android.content.SharedPreferences;
11
import android.content.pm.PackageManager;
12
import android.os.AsyncTask;
13
import android.os.Build;
14
import android.os.RemoteException;
15
import android.provider.Settings;
16
import android.support.v4.app.ActivityCompat;
17
import android.util.Log;
18
19
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
20
import com.google.android.gms.common.ConnectionResult;
21
import com.google.android.gms.common.GoogleApiAvailability;
22
23
import org.altbeacon.beacon.Beacon;
24
import org.altbeacon.beacon.BeaconConsumer;
25
import org.altbeacon.beacon.BeaconManager;
26
import org.altbeacon.beacon.BeaconParser;
27
import org.altbeacon.beacon.RangeNotifier;
28
import org.altbeacon.beacon.Region;
29
import org.altbeacon.beacon.logging.LogManager;
30
import org.altbeacon.beacon.logging.Loggers;
31
import org.altbeacon.beacon.powersave.BackgroundPowerSaver;
32
import org.altbeacon.beacon.startup.BootstrapNotifier;
33
import org.altbeacon.beacon.startup.RegionBootstrap;
34
import org.json.JSONArray;
35
import org.json.JSONException;
36
import org.json.JSONObject;
37
38
import java.io.UnsupportedEncodingException;
39
import java.util.Arrays;
40
import java.util.Collection;
41
import java.util.HashMap;
42
import java.util.HashSet;
43
import java.util.Iterator;
44
import java.util.Locale;
45
import java.util.Map;
46
import java.util.Set;
47
import java.util.UUID;
48
49
import okhttp3.ResponseBody;
50
import retrofit2.Call;
51
import retrofit2.Callback;
52
import retrofit2.Response;
53
import retrofit2.Retrofit;
54
import retrofit2.http.FieldMap;
55
import retrofit2.http.FormUrlEncoded;
56
import retrofit2.http.GET;
57
import retrofit2.http.POST;
58
import retrofit2.http.QueryMap;
59
60
public enum AreaMetrics implements BootstrapNotifier, BeaconConsumer, Callback<ResponseBody> {
61
INSTANCE;
62
63
private static final String TAG = "AMS";
64
private static final String SNIPPET_VERSION = "1.8";
65
private static String AM_SNIPPET_STORE = "AM_SNIPPET_STORE";
66
private static String AM_BEACON_BATCH = "AM_BEACON_BATCH";
67
private static String AM_BATCH_LAST_SENT = "AM_BATCH_LAST_SENT";
68
69
private String pubID = null;
70
private Context myContext = null;
71
private String adId = null;
72
private String vendorID = null;
73
private RegionBootstrap regionBootstrap;
74
private BackgroundPowerSaver backgroundPowerSaver;
75
private BeaconManager beaconManager;
76
private Set<String> rangingRegions = new HashSet<>();
77
private Set<String> amUUIDs = new HashSet<>(Arrays.asList("B9407F30-F5F8-466E-AFF9-25556B577272"));
78
private Map<String, Long> lastSentBeacons = new HashMap<>();
79
private boolean currentlySendingBatch = false;
80
81
// Network API Declaration
82
83
public interface AMAPIService {
84
@GET("user_init")
85
Call<ResponseBody> userInit(@QueryMap Map<String, String> params);
86
@FormUrlEncoded
87
@POST("beacon_batch")
88
Call<ResponseBody> beaconBatch(@FieldMap Map<String, String> params);
89
}
90
private Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.areametrics.com/v3/").build();
91
private AMAPIService api = retrofit.create(AMAPIService.class);
92
93
@Override
94
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
95
String url = call.request().url().toString();
96
if (url.endsWith("beacon_batch")) {
97
if (response.isSuccessful()) {
98
clearStoredBeacons();
99
updateStoredLastSend();
100
}
101
currentlySendingBatch = false;
102
}
103
if (BuildConfig.DEBUG) Log.d(TAG, "Response: " + response.message());
104
}
105
106
@Override
107
public void onFailure(Call<ResponseBody> call, Throwable t) {
108
String url = call.request().url().toString();
109
if (url.endsWith("beacon_batch")) {
110
currentlySendingBatch = false;
111
}
112
if (BuildConfig.DEBUG) Log.d(TAG, "Failure: " + t.getMessage());
113
}
114
115
// Start Service
116
117
public void startService(Application application, String publisherID) {
118
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
119
if (BuildConfig.DEBUG) Log.d(TAG, "Early return out of startService due to running KitKat");
120
return;
121
}
122
123
if (application == null || publisherID == null) {
124
throw new IllegalArgumentException("Called startService without Publisher ID or Application context");
125
}
126
127
if (!publisherID.endsWith("80")) {
128
throw new IllegalArgumentException("Called startService with invalid Android Publisher ID");
129
}
130
131
pubID = publisherID;
132
myContext = application;
133
134
new FetchAdID().execute();
135
136
if (isUserGDPR()) {
137
return;
138
}
139
140
LogManager.setLogger(Loggers.empty());
141
LogManager.setVerboseLoggingEnabled(false);
142
143
//beacon monitoring
144
beaconManager = BeaconManager.getInstanceForApplication(myContext);
145
beaconManager.getBeaconParsers().add(new BeaconParser("ibeacon").setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24")); //ibeacon
146
beaconManager.setRegionStatePersistenceEnabled(false);
147
Region region = new Region("AnyBeacon", null, null, null);
148
regionBootstrap = new RegionBootstrap(this, region);
149
backgroundPowerSaver = new BackgroundPowerSaver(myContext);
150
beaconManager.bind(this);
151
152
beaconManager.setForegroundScanPeriod(1_100L);
153
beaconManager.setForegroundBetweenScanPeriod(5_000L);
154
beaconManager.setBackgroundScanPeriod(8_000L);
155
beaconManager.setBackgroundBetweenScanPeriod(300_000L);
156
try {
157
beaconManager.updateScanPeriods();
158
} catch (RemoteException e) {
159
if (BuildConfig.DEBUG) Log.d(TAG, "RemoteException in updateScanPeriods()");
160
}
161
162
sendBeaconBatchIfNeeded();
163
164
if (BuildConfig.DEBUG) Log.d(TAG, "AreaMetrics Initialized Successfully!");
165
}
166
167
// AdID Fetch
168
169
private class FetchAdID extends AsyncTask<Void, Void, String> {
170
@Override
171
protected String doInBackground(Void... params) {
172
String gid = null;
173
if (isGooglePlayServicesAvailable(myContext)) {
174
try {
175
AdvertisingIdClient.Info adInfo = AdvertisingIdClient.getAdvertisingIdInfo(myContext);
176
if (adInfo != null && adInfo.getId() != null) {
177
gid = adInfo.getId();
178
}
179
} catch (Exception e) {
180
if (BuildConfig.DEBUG) Log.d(TAG, "Getting Ad Client Failed: " + e.getMessage());
181
}
182
}
183
return gid;
184
}
185
@Override
186
protected void onPostExecute (String s) {
187
if (s != null) {
188
adId = s;
189
}
190
sendUserInit();
191
}
192
}
193
194
public final boolean isGooglePlayServicesAvailable(Context context) {
195
GoogleApiAvailability availability = GoogleApiAvailability.getInstance();
196
if (availability != null) {
197
int resultCode = availability.isGooglePlayServicesAvailable(context);
198
return resultCode == ConnectionResult.SUCCESS;
199
} else {
200
return false;
201
}
202
}
203
204
// Beacon Monitoring
205
206
@Override
207
public void didEnterRegion(Region region) {
208
sendBeaconBatchIfNeeded();
209
if (!rangingRegions.contains(region.getUniqueId())) {
210
rangingRegions.add(region.getUniqueId());
211
try {
212
beaconManager.startRangingBeaconsInRegion(region);
213
} catch (RemoteException e) {
214
if (BuildConfig.DEBUG) Log.d(TAG, "startRangingBeaconsInRegion failed due to unbound beacon manager");
215
}
216
}
217
}
218
219
@Override
220
public void didExitRegion(Region region) {
221
sendBeaconBatchIfNeeded();
222
try {
223
beaconManager.stopRangingBeaconsInRegion(region);
224
} catch (RemoteException e) {
225
if (BuildConfig.DEBUG) Log.d(TAG, "stopRangingBeaconsInRegion failed due to unbound beacon manager");
226
}
227
if (region.getUniqueId() != null) {
228
rangingRegions.remove(region.getUniqueId());
229
}
230
}
231
232
@Override
233
public void didDetermineStateForRegion(int state, Region region) {
234
sendBeaconBatchIfNeeded();
235
}
236
237
@Override
238
public void onBeaconServiceConnect() {
239
beaconManager.addRangeNotifier(new RangeNotifier() {
240
@Override
241
public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
242
for (Beacon beacon : beacons) {
243
if (beacon.getIdentifiers().size() < 3) {
244
continue;
245
}
246
storeBeaconIfNeeded(beacon);
247
}
248
}
249
});
250
251
try {
252
beaconManager.startRangingBeaconsInRegion(new Region("AnyBeacon", null, null, null));
253
} catch (RemoteException e) {
254
if (BuildConfig.DEBUG) Log.d(TAG, "Failed to start ranging beacons");
255
}
256
}
257
258
public Context getApplicationContext() {
259
return myContext;
260
}
261
262
public void unbindService(ServiceConnection var1) {
263
getApplicationContext().unbindService(var1);
264
}
265
266
public boolean bindService(Intent var1, ServiceConnection var2, int var3) {
267
return getApplicationContext().bindService(var1, var2, var3);
268
}
269
270
private void clearStoredBeacons() {
271
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
272
SharedPreferences.Editor editor = prefs.edit();
273
editor.putStringSet(AM_BEACON_BATCH, new HashSet<String>());
274
editor.apply();
275
}
276
277
private void storeBeaconIfNeeded(Beacon beacon) {
278
String beaconID = String.format(Locale.US, "%s.%s.%s",
279
beacon.getId1().toString(),
280
beacon.getId2().toString(),
281
beacon.getId3().toString());
282
283
if (lastSentBeacons.containsKey(beaconID)) {
284
long lastTime = lastSentBeacons.get(beaconID);
285
long delay = amUUIDs.contains(beacon.getId1().toString()) ? 5_000 : 5_000;
286
if (System.currentTimeMillis() - lastTime < delay) {
287
return; // early return to limit excess
288
}
289
}
290
lastSentBeacons.put(beaconID, System.currentTimeMillis());
291
292
try {
293
JSONObject beaconJSON = new JSONObject();
294
long epochSeconds = System.currentTimeMillis() / 1000L;
295
beaconJSON.put("time", epochSeconds);
296
beaconJSON.put("uuid", beacon.getId1().toString());
297
beaconJSON.put("major", beacon.getId2().toInt());
298
beaconJSON.put("minor", beacon.getId3().toInt());
299
beaconJSON.put("rssi", beacon.getRssi());
300
beaconJSON.put("tx_power", beacon.getTxPower());
301
beaconJSON.put("avg_rssi", beacon.getRunningAverageRssi());
302
beaconJSON.put("btype", beacon.getParserIdentifier());
303
beaconJSON.put("manufacturer", beacon.getManufacturer());
304
beaconJSON.put("beacon_accuracy", beacon.getDistance());
305
beaconJSON.put("address", beacon.getBluetoothAddress());
306
beaconJSON.put("num_rssi", beacon.getMeasurementCount());
307
beaconJSON.put("name", beacon.getBluetoothName());
308
309
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
310
Set<String> batchStringSet = prefs.getStringSet(AM_BEACON_BATCH, new HashSet<String>());
311
312
int batchOverflow = batchStringSet.size() - 100;
313
if (batchOverflow > 0) {
314
int index = 0;
315
for (Iterator<String> i = batchStringSet.iterator(); i.hasNext();) {
316
i.next();
317
if (index <= batchOverflow) {
318
i.remove();
319
} else {
320
break;
321
}
322
index++;
323
}
324
}
325
326
batchStringSet.add(beaconJSON.toString());
327
328
SharedPreferences.Editor editor = prefs.edit();
329
editor.putStringSet(AM_BEACON_BATCH, batchStringSet);
330
editor.apply();
331
} catch (JSONException e) {
332
if (BuildConfig.DEBUG) Log.d(TAG, "JSONException while storing Beacon in batch!");
333
}
334
sendBeaconBatchIfNeeded();
335
}
336
337
private void updateStoredLastSend() {
338
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
339
SharedPreferences.Editor editor = prefs.edit();
340
editor.putLong(AM_BATCH_LAST_SENT, System.currentTimeMillis());
341
editor.apply();
342
}
343
344
private void sendBeaconBatchIfNeeded() {
345
SharedPreferences prefs = getApplicationContext().getSharedPreferences(AM_SNIPPET_STORE, Context.MODE_PRIVATE);
346
Set<String> batchStringSet = prefs.getStringSet(AM_BEACON_BATCH, new HashSet<String>());
347
if (batchStringSet.size() > 0) {
348
long batchLastSend = prefs.getLong(AM_BATCH_LAST_SENT, 0);
349
if (System.currentTimeMillis() - batchLastSend > 1800_000) {
350
sendBeaconBatchToServer(batchStringSet);
351
}
352
}
353
}
354
355
private void sendBeaconBatchToServer(Set<String> batchStringSet) {
356
if (currentlySendingBatch) {
357
return;
358
}
359
currentlySendingBatch = true;
360
361
Map<String, String> params = new HashMap<>();
362
params.put("pub_id", pubID);
363
params.put("os", "android");
364
params.put("os_ver", Build.VERSION.RELEASE);
365
params.put("model", Build.MANUFACTURER + " " + Build.MODEL);
366
params.put("user_locale", Locale.getDefault().getCountry());
367
params.put("snippet_ver", SNIPPET_VERSION);
368
if (!isUserGDPR()) {
369
if (getVendorID() != null) {
370
params.put("vendor_id", getVendorID());
371
}
372
if (adId != null) {
373
params.put("ad_id", adId);
374
}
375
params.put("agent", System.getProperty("http.agent"));
376
}
377
378
try {
379
JSONArray batchArray = new JSONArray();
380
for (String jsonStr : batchStringSet) {
381
batchArray.put(new JSONObject(jsonStr));
382
}
383
params.put("batch", batchArray.toString());
384
385
Call<ResponseBody> call = api.beaconBatch(params);
386
call.enqueue(this);
387
} catch (JSONException e) {
388
clearStoredBeacons();
389
currentlySendingBatch = false;
390
if (BuildConfig.DEBUG) Log.d(TAG, "JSONException while assembling Beacon Batch for sending!");
391
}
392
}
393
394
private void sendUserInit() {
395
Map<String, String> params = new HashMap<>();
396
params.put("pub_id", pubID);
397
params.put("os", "android");
398
params.put("os_ver", Build.VERSION.RELEASE);
399
params.put("model", Build.MANUFACTURER + " " + Build.MODEL);
400
params.put("locale", Locale.getDefault().getCountry());
401
params.put("snippet_ver", SNIPPET_VERSION);
402
if (getVendorID() != null) {
403
params.put("vendor_id", getVendorID());
404
}
405
if (!isUserGDPR() && adId != null) {
406
params.put("ad_id", adId);
407
}
408
params.put("loc_permission", getLocPermission());
409
410
Call<ResponseBody> call = api.userInit(params);
411
call.enqueue(this);
412
}
413
414
boolean isUserGDPR() {
415
Set<String> countries = getGDPRCountries();
416
String device = Locale.getDefault().getCountry();
417
return device != null && countries.contains(device);
418
}
419
420
private Set<String> getGDPRCountries() {
421
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"));
422
}
423
424
private String getVendorID() {
425
if (vendorID == null) {
426
try {
427
if (myContext != null && myContext.getContentResolver() != null) {
428
String androidId = Settings.Secure.getString(myContext.getContentResolver(), Settings.Secure.ANDROID_ID);
429
if (androidId != null) {
430
vendorID = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")).toString();
431
}
432
}
433
} catch (UnsupportedEncodingException e) {
434
if (BuildConfig.DEBUG) Log.d(TAG, "VendorID encoding error");
435
}
436
}
437
return vendorID;
438
}
439
440
private String getLocPermission() {
441
int fine = ActivityCompat.checkSelfPermission(myContext, Manifest.permission.ACCESS_FINE_LOCATION);
442
int coarse = ActivityCompat.checkSelfPermission(myContext, Manifest.permission.ACCESS_COARSE_LOCATION);
443
if (fine == PackageManager.PERMISSION_GRANTED || coarse == PackageManager.PERMISSION_GRANTED) {
444
return "authorized";
445
} else {
446
return "denied";
447
}
448
}
449
}
Copied!

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
1
import android.app.Application;
2
public class MyApp extends Application {
3
@Override
4
public void onCreate() {
5
super.onCreate();
6
AreaMetrics.INSTANCE.startService(this, "PUBLISHER_ID");
7
}
8
}
Copied!
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 modified 2yr ago