alin ciupeiu
alin ciupeiu

Reputation: 71

Application cycles between didEnterRegion() and didExitRegion() even when the device stays stationary near the beacon

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:

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:

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

Upvotes: 1

Views: 518

Answers (3)

alin ciupeiu
alin ciupeiu

Reputation: 71

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

Upvotes: 0

davidgyoung
davidgyoung

Reputation: 64916

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.

Upvotes: 1

Andrea Ebano
Andrea Ebano

Reputation: 573

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.

Upvotes: 0

Related Questions