1
votes

I am using AltBeacon Android Library (I reproduced issue with v2.9.2; and also with v2.11) for integrating with iBeacon devices provided by Onyx and kontact.io.

The library seems to work very well, but I seem to have an issue with it for which I could not find an acceptable solution.

Here are some more details about how I use AltBeacon Library and about the issue:

  • Device is stationary near the beacon
  • Bluetooth on
  • Application runs in foreground
  • The BeaconManager is configured to scan in foreground mode with the following settings:

    BeaconManager.setRegionExitPeriod(30000L);
    beaconManager.setBackgroundBetweenScanPeriod(120000L);
    beaconManager.setForegroundScanPeriod(5000L);
    beaconManager.setForegroundBetweenScanPeriod(10000L);
    beaconManager.getBeaconParsers().add(
    new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
    
  • Application sets the BeaconManager in foreground mode

    beaconManager.setBackgroundMode(false);
    
  • Application bounds to the BeaconManager

    beaconManager.bind(…)
    
  • When onBeaconServiceConnect() is triggered, the application starts monitoring beacons in specific regions (the list of beacons I want to monitor is known, static; I use a list of regions, one different region for each beacon I want to monitor)

    beaconManager.startMonitoringBeaconsInRegion(region);
    
  • When device enters beacon region (didEnterRegion() is called) application starts ranging for entered region

    beaconManager.startRangingBeaconsInRegion(region);
    
  • Beacon is detected (didRangeBeaconsInRegion() is called for corresponding beacon)

  • Application switched beacon scanning to background mode:

    beaconManager.setBackgroundMode(true);
    
  • After a few minutes, the didExitRegion() is called even if the device and the beacon were not moved and the application remained in the same state.

I have found two Stackoverflow issues which describe the same issue:

  1. AltBeacon unstable for OnyxBeacons, cycling through didEnterRegion and didExitRegion repeatedly

  2. http://stackoverflow.com/questions/40835671/altbeacon-reference-app-and-multiple-exit-entry-calls

The workaround that I currently use is the one suggested in the Stackoverflow issues:

  • I have updated beacon Advertising Frequency value from 1000 ms to 100 ms.

Once the frequency is increased, everything seems to work fine, but the solution is not acceptable because the battery life of the beacon is drastically impaired.

All the beacon scanning is performed in background (i.e. no Activity is used):

import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconConsumer;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.Identifier;
import org.altbeacon.beacon.MonitorNotifier;
import org.altbeacon.beacon.RangeNotifier;
import org.altbeacon.beacon.Region;
import org.altbeacon.beacon.powersave.BackgroundPowerSaver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class BeaconDataProvider implements BeaconConsumer, RangeNotifier, MonitorNotifier {

  private final Logger LOGGER = LogFactory.get(this);
  private final Context applicationContext;
  private final BeaconIdentifierFactory beaconIdentifierFactory;
  private final BeaconScanningListener beaconScanningListener;

  private BeaconManager beaconManager;
  private Collection<Region> targetedRegions;

  /**
   * This field is used for improving battery consumption. Do not remove it.
   */
  @SuppressWarnings({"unused", "FieldCanBeLocal"})
  private BackgroundPowerSaver backgroundPowerSaver;

  public BeaconDataProvider(Context applicationContext, BeaconIdentifierFactory beaconIdentifierFactory,
      BeaconScanningListener beaconScanningListener) {
    LOGGER.v("BeaconDataProvider - new instance created.");
    this.applicationContext = applicationContext;
    this.beaconIdentifierFactory = beaconIdentifierFactory;
    this.beaconScanningListener = beaconScanningListener;
    beaconManager = BeaconManager.getInstanceForApplication(applicationContext);
    LOGGER.v("BeaconManager hashCode=%s", beaconManager.hashCode());
    BeaconManager.setRegionExitPeriod(30000L);
    beaconManager.setBackgroundBetweenScanPeriod(120000L);
    beaconManager.setForegroundScanPeriod(5000L);
    beaconManager.setForegroundBetweenScanPeriod(10000L);
    beaconManager.getBeaconParsers().add(
        new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"));
    backgroundPowerSaver = new BackgroundPowerSaver(applicationContext);
  }

  public void setBackgroundMode() {
    LOGGER.i("setBackgroundMode()");
    beaconManager.setBackgroundMode(true);
  }

  public void setForegroundMode() {
    LOGGER.i("setForegroundMode()");
    beaconManager.setBackgroundMode(false);
  }

  public boolean checkAvailability() {
    return android.os.Build.VERSION.SDK_INT >= 18 && applicationContext.getPackageManager()
        .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);

  }

  public boolean isBluetoothEnabled() {
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    boolean result = mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
    LOGGER.i("isBluetoothEnabled() -> %s", result);
    return result;
  }

  public boolean isLocationPermissionGranted(Context context) {
    return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
        && context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
        == PackageManager.PERMISSION_GRANTED);
  }

  public void startScanning(Collection<BeaconIdentifier> targetedBeacons) {
    LOGGER.i("startScanning()");
    if (!beaconManager.isBound(this)) {
      this.targetedRegions = getRegionsForTargetedBeacons(targetedBeacons);
      beaconManager.bind(this);
    }
    else {
      LOGGER.i("Scanning already started.");
    }
  }

  @NonNull
  private List<Region> getRegionsForTargetedBeacons(Collection<BeaconIdentifier> beaconIdentifiers) {
    List<Region> regions = new ArrayList<>();
    for (BeaconIdentifier beaconIdentifier : beaconIdentifiers) {
      try {
        Region region = new Region(beaconIdentifier.getRegionId(), Identifier.parse(beaconIdentifier.getUuid()),
            Identifier.parse(String.valueOf(beaconIdentifier.getMajor())),
            Identifier.parse(String.valueOf(beaconIdentifier.getMinor())));
        regions.add(region);
      }
      catch (Exception e) {
        LOGGER.e("Caught exception.", e);
        LOGGER.w("Failed to create region for beaconIdentifier=%s", beaconIdentifier.getCallParamRepresentation());
      }
    }
    return regions;
  }

  public void stopScanning() {
    LOGGER.i("stopScanning()");
    if (beaconManager.isBound(this)) {
      for (Region region : targetedRegions) {
        try {
          beaconManager.stopMonitoringBeaconsInRegion(region);
        }
        catch (RemoteException e) {
          LOGGER.e("Caught exception", e);
        }
      }
      beaconManager.unbind(this);
    }
  }

  @Override
  public void didEnterRegion(Region region) {
    LOGGER.v("didEnterRegion(region=%s)", region);
    beaconScanningListener.onEnterRegion(region.getUniqueId());
    try {
      beaconManager.startRangingBeaconsInRegion(region);
    }
    catch (RemoteException e) {
      LOGGER.e("Caught Exception", e);
    }
  }

  @Override
  public void didExitRegion(Region region) {
    LOGGER.v("didExitRegion(region=%s)", region);
    beaconScanningListener.onExitRegion(region.getUniqueId());
    try {
      beaconManager.stopRangingBeaconsInRegion(region);
    }
    catch (RemoteException e) {
      LOGGER.e("Error", e);
    }
  }

  @Override
  public void didDetermineStateForRegion(int state, Region region) {
    LOGGER.v("didDetermineStateForRegion(state=%s, region=%s)", state, region);
  }

  @Override
  public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
    LOGGER.v("didRangeBeaconsInRegion(size=%s, region=%s, regionUniqueId=%s)", beacons.size(), region,
        region.getUniqueId());
    if (beacons.size() > 0) {
      beaconScanningListener.onBeaconsInRange(beaconIdentifierFactory.from(beacons, region.getUniqueId()));
    }
  }

  @Override
  public void onBeaconServiceConnect() {
    LOGGER.v("onBeaconServiceConnect()");
    beaconManager.addRangeNotifier(this);
    beaconManager.addMonitorNotifier(this);
    for (Region region : targetedRegions) {
      try {
        beaconManager.startMonitoringBeaconsInRegion(region);
      }
      catch (RemoteException e) {
        LOGGER.e("Caught exception", e);
      }
    }
  }

  @Override
  public Context getApplicationContext() {
    return applicationContext;
  }

  @Override
  public void unbindService(ServiceConnection serviceConnection) {
    LOGGER.v("unbindService()");
    applicationContext.unbindService(serviceConnection);
  }

  @Override
  public boolean bindService(Intent intent, ServiceConnection serviceConnection, int i) {
    LOGGER.v("bindService()");
    return applicationContext.bindService(intent, serviceConnection, i);
  }
}

public class BeaconIdentifier {

  private final String uuid;
  private final int major;
  private final int minor;
  private String regionId;

  public BeaconIdentifier(String uuid, int major, int minor) {
    this.uuid = uuid;
    this.major = major;
    this.minor = minor;
  }

  public int getMinor() {
    return minor;
  }

  public int getMajor() {
    return major;
  }

  public String getUuid() {
    return uuid;
  }

  public String getCallParamRepresentation() {
    return (uuid + "_" + major + "_" + minor).toUpperCase();
  }

  public String getRegionId() {
    return regionId;
  }

  public void setRegionId(String regionId) {
    this.regionId = regionId;
  }

  @Override
  public boolean equals(Object o) {
    if (o != null) {
      if (o instanceof BeaconIdentifier) {
        BeaconIdentifier other = (BeaconIdentifier) o;
        return this == other || (this.uuid.equalsIgnoreCase(other.uuid)
            && this.major == other.major && this.minor == other.minor);
      }
      else {
        return false;
      }
    }
    else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    int result = 17;
    result = 31 * result + (uuid != null ? uuid.toUpperCase().hashCode() : 0);
    result = 31 * result + major;
    result = 31 * result + minor;
    return result;
  }

  @Override
  public String toString() {
    return "BeaconIdentifier{" +
        "uuid='" + uuid + '\'' +
        ", major=" + major +
        ", minor=" + minor +
        ", regionId='" + regionId + '\'' +
        '}';
  }
}

The BeaconDataProvider is used as a single instance per application; It is instantiated by Dagger 2 when the Android Application is created. It has @ApplicationScope lifecycle.

The beacon scanning is first started`in foreground mode from an Android IntentService:

    beaconDataProvider.setForegroundMode();    
    beaconDataProvider.startScanning(targetedBeacons);

Once the device enters the region and the beacon is detected, beacon scanning is switched to background mode:

    beaconDataProvider.setBackgroundMode();    

At first I thought there was something wrong with the Onyx Beacons I was using, but I could reproduce the same issue with the Kontact IO Beacons.

  1. Do you have any suggestions?

  2. Am I miss-using the AltBeacon Android Library?

Thanks, Alin

3
Post your RangingActivity please.Andrea Ebano
@AndreaEbano, I have edited my post and included the main classes used for integrating with AltBeacon. Please let me know if something is not clear.alin ciupeiu

3 Answers

1
votes

The fundamental cause of a call to didExitRegion() is the fact that no BLE beacon advertisement packets matching the region were received by the Android bluetooth stack in the previous 10 seconds. (Note: This value is configurable with BeaconManager.setRegionExitPeriod(...).)

There are several things that could be causing these spurious didExitRegion() calls:

  1. A beacon is not advertising frequently enough.
  2. A beacon is advertising with a very low radio signal.
  3. There is too much radio noise in the vicinity for reliable detections.
  4. The receiving device has a poor bluetooth antenna design causing weaker signals to not get detected.
  5. The receiving device is too far away to reliably detect the beacon.
  6. The foregroundScanPeriod or backgroundScanPeriod is set too short to get a guaranteed detection

Given the setup you've described, I suspect that when you have the beacon transmitting at 1Hz, some combination of 1-4 is causing the problem. You will have to experiment with each of these variables to see if you can isolate the problem to one predominant issue. But again, more than one may be at play at the same time.

Understand that even under good conditions only 80-90 percent of beacons packets transmitted over the air are received by a typical Android device. Because of this, if you have a setup where only 1-5 beacon packets are typically received in a 10 second period, you'll still sometimes get exit events if you get unlucky and a few packets in a row get corrupted by radio noise. There is no way to guarantee this won't happen. You can just make it statistically more unlikely by setting up your system so under nominal conditions it receives as many packets as possible in a 10 second period, so this becomes more unlikely.

Increasing the advertising rate is the easiest way to fix this, because it gives you more statistical chances of getting packets detected in any 10 second period. But as you have seen, there is a tradeoff in terms of battery life.

If you want do preserve battery life but don't care about the time it takes to get a didExitRegion callback, then you may want to modify BeaconManager.setRegionExitPeriod(...) to 30,000 milliseconds or more until the problem goes away.

The above discussion is specific to the configuration of the Android Beacon Library, the same theoretical ideas apply to any beacon detection framework including iOS Core Location. You sometimes see spurious exit events with that framework as well.

0
votes

I think the problem is here:

beaconManager.setForegroundScanPeriod(5000L);
beaconManager.setForegroundBetweenScanPeriod(10000L);

You should generally set the scanPeriod to 5100 ms or more, because beacons that advertise have a slight chance of being missed if their transmission is always on the boundary of when you start and stop scanning.

So try:

beaconManager.setForegroundScanPeriod(5100L);
beaconManager.setForegroundBetweenScanPeriod(10000L);

Hope it helps. Let me know if works.

0
votes

As a workaround to this issue, I have implemented some extra logic to consider a didExitRegion() event only if the corresponding didEnterRegion() is not called in a certain time interval (5 minutes in my case, but this can be adjusted).