android - 即使设备在信标附近保持静止,应用程序也会在 didEnterRegion() 和 didExitRegion() 之间循环

标签 android ibeacon battery altbeacon

我正在使用 AltBeacon Android 库(我重现了 v2.9.2 的问题;还有 v2.11)与 Onyx 和 kontact.io 提供的 iBeacon 设备集成。

该库似乎工作得很好,但我似乎遇到了一个问题,我找不到可接受的解决方案。

以下是有关我如何使用 AltBeacon 库和问题的更多详细信息:

  • 设备在信标附近静止
  • 开启蓝牙
  • 应用在前台运行
  • BeaconManager 配置为使用以下设置在前台模式下扫描:

    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"));
    
  • 应用程序将 BeaconManager 设置为前台模式

    beaconManager.setBackgroundMode(false);
    
  • 应用程序绑定(bind)到 BeaconManager

    beaconManager.bind(…)
    
  • onBeaconServiceConnect() 被触发时,应用程序开始监控特定区域的信标(我要监控的信标列表是已知的,静态的;我使用区域列表,一个我要监控的每个信标的不同区域)

    beaconManager.startMonitoringBeaconsInRegion(region);
    
  • 当设备进入信标区域时(didEnterRegion() 被调用)应用程序开始对进入的区域进行测距

    beaconManager.startRangingBeaconsInRegion(region);
    
  • Beacon 被检测到(didRangeBeaconsInRegion() 为对应的 beacon 调用)

  • 应用程序将信标扫描切换到后台模式:

    beaconManager.setBackgroundMode(true);
    
  • 几分钟后,即使设备和信标未移动且应用程序保持相同状态,didExitRegion() 也会被调用。

我发现了两个描述相同问题的 Stackoverflow 问题:

  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

我目前使用的解决方法是 Stackoverflow 问题中建议的解决方法:

  • 我已将信标 广告频率 值从 1000 毫秒更新为 100 毫秒

一旦频率增加,似乎一切正常,但是 该解决方案是 Not Acceptable ,因为信标的电池生命周期是 严重受损

所有信标扫描都在后台执行(即不使用 Activity):

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 + '\'' +
        '}';
  }
}

BeaconDataProvider 被用作每个应用程序的单个实例;它在创建 Android 应用程序时由 Dagger 2 实例化。它有@ApplicationScope 生命周期。

信标扫描首先在 前台模式 中从 Android IntentService 开始:

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

一旦设备进入该区域并检测到信标,信标扫描就会切换到后台模式:

    beaconDataProvider.setBackgroundMode();    

起初我以为我使用的 Onyx Beacons 有问题,但我可以用 Kontact IO Beacons 重现同样的问题。

  1. 您有什么建议吗?

  2. 我是否误用了 AltBeacon Android 库?

谢谢, 阿林

最佳答案

调用 didExitRegion() 的根本原因是在过去 10 秒内 Android 蓝牙堆栈没有收到与该区域匹配的 BLE 信标广告包。 (注意:此值可使用 BeaconManager.setRegionExitPeriod(...) 进行配置。)

有几件事可能导致这些虚假的 didExitRegion() 调用:

  1. 信标没有足够频繁地发布广告。
  2. 信标通过非常低的 radio 信号进行广告。
  3. 附近的 radio 噪音太大,无法进行可靠的检测。
  4. 接收设备的蓝牙天线设计不佳,导致无法检测到较弱的信号。
  5. 接收设备距离太远,无法可靠地检测到信标。
  6. foregroundScanPeriod 或 backgroundScanPeriod 设置得太短,无法保证检测

鉴于您所描述的设置,我怀疑当您以 1Hz 的频率传输信标时,1-4 的某种组合会导致问题。您将不得不对这些变量中的每一个进行试验,以查看是否可以将问题隔离为一个主要问题。但同样,可能不止一个人同时在玩。

了解即使在良好的条件下,典型的 Android 设备也只有 80-90% 的无线传输信标数据包被接收。正因为如此,如果您的设置通常在 10 秒内仅收到 1-5 个信标数据包,您仍然有时会收到退出事件,如果您不走运并且行被 radio 噪声破坏。没有办法保证这不会发生。您可以通过设置您的系统来使其在统计上更不可能,因此在标称条件下它会在 10 秒内接收尽可能多的数据包,因此这变得更不可能。

提高广告率是解决这个问题最简单的方法,因为它可以让您在任何 10 秒内有更多的统计机会检测到数据包。但正如您所见,在电池生命周期方面存在权衡。

如果你想保留电池生命周期但不关心获得 didExitRegion 回调所需的时间,那么你可能需要将 BeaconManager.setRegionExitPeriod(...) 修改为 30,000 毫秒或更多,直到问题消失。

以上讨论是针对Android Beacon Library的配置,同样的理论思想适用于包括iOS Core Location在内的任何信标检测框架。您有时也会看到该框架的虚假退出事件。

关于android - 即使设备在信标附近保持静止,应用程序也会在 didEnterRegion() 和 didExitRegion() 之间循环,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45479417/

相关文章:

安卓温度区?

android - 以从左到右模式开始 Activity

android - Android 设备运行时的 iBeacon 准确性

ios - 信标和广告

ios - 在 iOS 应用程序中使用 iBeacons 时出现电池耗尽问题

ios - 有没有办法使用 objective-c 访问 iOS 中的 "time since last full charge"值?

ios - 如何获取蓝牙连接设备的电池电量

android - 将 EditText/TextView 置于 MapView Android 之上

java - 我的 Android 应用程序正在耗尽电池电量吗?

c# - Xamarin Forms Android 键盘向上移动整个页面