Search code examples
androidaltbeacon

Start beacon ranging on foreground when entering region


Goal

EddyStone EID beacon detection when app is in the background. As soon as the user enters a beacon region, the application should start ranging and make a http call to my server so I am informed of the detection. Even for short visits.

Because of the background scanning limitations on Android I was thinking of using a region bootstrap and starting the foreground service as soon as I enter a beacon region. This has my preference over using the foreground service directly so I don't have the notification visible all the time.

Issue

I've based my application on the AltBeacon reference application. I tried starting the foreground service as soon as the user enters the region. Foreground service gets started, but the ranging notifier doesn't show any beacon detections. Alternative that I tried is starting the foregroundservice and ranging in the didDetermineState method callback, but this doesn't work out because I have to enable and disable the region bootstrap to do so, and this will trigger the didDetermineState callback method again.

How can I detect beacons in the background (without a delay) and start ranging without using the foreground service all the time?

Code + Log

public class AppController extends MultiDexApplication implements BootstrapNotifier,
    RangeNotifier, BeaconConsumer {

  private static final String TAG = "BEACON:";
  private static AppComponent appComponent;
  private static AppController instance;

  private RegionBootstrap regionBootstrap;
  private BackgroundPowerSaver backgroundPowerSaver;
  private boolean isScanningOnForeground = false;
  private BeaconManager beaconManager;

  @Override
  public void onCreate() {
    super.onCreate();
    beaconManager = BeaconManager.getInstanceForApplication(this);
    beaconManager.setDebug(true);
    beaconManager.getBeaconParsers().clear();
    beaconManager.getBeaconParsers()
        .add(new BeaconParser().setBeaconLayout(BeaconParser.EDDYSTONE_UID_LAYOUT));
    Region region = new Region("backgroundRegion",
        null, null, null);
    regionBootstrap = new RegionBootstrap(this, region);
    backgroundPowerSaver = new BackgroundPowerSaver(this);
    instance = this;
    this.getAppComponent().inject(this);
  }

  public AppComponent getAppComponent() {
    if (appComponent == null) {
      appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();
    }
    return appComponent;
  }

  public static AppController getInstance() {
    return instance;
  }

  @Override
  public void didEnterRegion(Region region) {
    Log.d(TAG, "(didEnterRegion) Beacon detected in region!");
    startMonitoringOnForeground();
  }

  public void disableMonitoring() {
    if (regionBootstrap != null) {
      regionBootstrap.disable();
      regionBootstrap = null;
    }
  }

  public void enableMonitoring() {
    Region region = new Region("backgroundRegion",
        null, null, null);
    regionBootstrap = new RegionBootstrap(this, region);
  }

  private void enableRanging() {

    Log.d(TAG, "Enable ranging");
    beaconManager.removeAllRangeNotifiers();
    beaconManager.addRangeNotifier(this);
    try {
      beaconManager.startRangingBeaconsInRegion(new Region("rangingRegion",
          null, null, null));
      Log.d(TAG, "Ranging started..");
    } catch (RemoteException e) {
      Log.d(TAG, ">>>>>>>>>>> START RANGING EXCEPTION!");
      e.printStackTrace();
    }
  }

  private void disableRanging() {

    Log.d(TAG, "Disable ranging.");
    try {
      beaconManager.stopRangingBeaconsInRegion(new Region("rangingRegion",
          null, null, null));
    } catch (RemoteException e) {
      Log.d(TAG, ">>>>>>>>>>> START RANGING EXCEPTION!");

      e.printStackTrace();
    }
    beaconManager.removeAllRangeNotifiers();
  }

  private void startMonitoringOnForeground() {
    if (isScanningOnForeground) {
      Log.d(TAG, "Ignore method call, already scanning in foreground");
      return;
    }
    isScanningOnForeground = true;

    disableMonitoring();

    Notification.Builder builder = new Notification.Builder(this);
    builder.setSmallIcon(R.drawable.app_icon);
    builder.setContentTitle("Scanning for Beacons");
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      NotificationChannel channel = new NotificationChannel("My Notification Channel ID",
          "My Notification Name", NotificationManager.IMPORTANCE_DEFAULT);
      channel.setDescription("My Notification Channel Description");
      NotificationManager notificationManager = (NotificationManager) getSystemService(
          Context.NOTIFICATION_SERVICE);
      notificationManager.createNotificationChannel(channel);
      builder.setChannelId(channel.getId());
    }
    beaconManager.enableForegroundServiceScanning(builder.build(), 456);
    // For the above foreground scanning service to be useful, you need to disable
    // JobScheduler-based scans (used on Android 8+) and set a fast background scan
    // cycle that would otherwise be disallowed by the operating system.
    //
    beaconManager.setEnableScheduledScanJobs(false);
    beaconManager.setBackgroundBetweenScanPeriod(30000);
    beaconManager.setBackgroundScanPeriod(1100);

    enableRanging();
    enableMonitoring();
  }

  private void stopMonitoringOnForeground() {
    if (!isScanningOnForeground) {
      Log.d(TAG, "Not stopping since foreground scanning isn't active.");
      return;
    }
    isScanningOnForeground = false;

    disableMonitoring();
    beaconManager.disableForegroundServiceScanning();
    enableMonitoring();
    disableRanging();
  }

  @Override
  public void didExitRegion(Region region) {
    Log.d(TAG, "(didExitRegion) No beacons anymore");
    stopMonitoringOnForeground();
  }

  @Override
  public void didDetermineStateForRegion(int state, Region region) {
    Log.d(TAG,
        "Determine state for region " + (state == 1 ? "INSIDE" : "OUTSIDE (" + state + ")"));
  }

  @Override
  public void didRangeBeaconsInRegion(Collection<Beacon> collection, Region region) {
    Log.d(TAG, "Did range beacons in region");
    for (Beacon beacon : collection) {
      Log.d(TAG, "Beacon:  " + beacon.getId1().toString());
    }
  }

  @Override
  public void onBeaconServiceConnect() {
    Log.d(TAG, "OnBeaconServiceConnect");
  }
} 
D/BEACON:: Determine state for region OUTSIDE (0)
D/BEACON:: Determine state for region INSIDE
D/BEACON:: (didEnterRegion) Beacon detected in region!
D/BEACON:: Enable ranging
D/BEACON:: Ranging started..
D/BEACON:: Determine state for region OUTSIDE (0)
D/BEACON:: Determine state for region INSIDE
D/BEACON:: (didEnterRegion) Beacon detected in region!
D/BEACON:: Ignore method call, already scanning in foreground
D/BEACON:: (didEnterRegion) Beacon detected in region!
D/BEACON:: Ignore method call, already scanning in foreground

Solution

  • The technique you show is probably close to working. The main problem is that you cannot start ranging until after the foreground service is "bound".

    The foreground service takes a brief time to start up, and only once it is started can you start ranging. A few options to try:

    1. You may be able to start ranging before you disable RegionBootstrap. Then leave the rest of your code as-is, and it should still be on when the service starts up. (Not positive this will work.)

    2. If the above does not work, try starting ranging only after you get a didDerermineState callback with isScanningOnForeground true. This will ensure the service is bound.

    3. The best and cleanest way to fix this is to stop using RegionBootstrap, as it does not provide a direct callback when the service is bound. The BeaconManager.bind(this) call does provide a onBeaconServiceConnect callback that can be used to start both monitoring and ranging. This option, however, requires the most code changes.

    You may also wish to set beacon manager.setDebug(true) and look for messages after ranging is started that give you a clue as to why it is not working.