/** * Radius Networks, Inc. * http://www.radiusnetworks.com * * @author David G. Young * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.altbeacon.beacon; import android.annotation.TargetApi; import android.app.Notification; import android.bluetooth.BluetoothManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.altbeacon.beacon.logging.LogManager; import org.altbeacon.beacon.logging.Loggers; import org.altbeacon.beacon.service.BeaconService; import org.altbeacon.beacon.service.Callback; import org.altbeacon.beacon.service.MonitoringStatus; import org.altbeacon.beacon.service.RangeState; import org.altbeacon.beacon.service.RangedBeacon; import org.altbeacon.beacon.service.RegionMonitoringState; import org.altbeacon.beacon.service.RunningAverageRssiFilter; import org.altbeacon.beacon.service.ScanJobScheduler; import org.altbeacon.beacon.service.SettingsData; import org.altbeacon.beacon.service.StartRMData; import org.altbeacon.beacon.service.scanner.NonBeaconLeScanCallback; import org.altbeacon.beacon.simulator.BeaconSimulator; import org.altbeacon.beacon.utils.ProcessUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; /** * A class used to set up interaction with beacons from an <code>Activity</code> or <code>Service</code>. * This class is used in conjunction with <code>BeaconConsumer</code> interface, which provides a callback * when the <code>BeaconService</code> is ready to use. Until this callback is made, ranging and monitoring * of beacons is not possible. * * In the example below, an Activity implements the <code>BeaconConsumer</code> interface, binds * to the service, then when it gets the callback saying the service is ready, it starts ranging. * * <pre><code> * public class RangingActivity extends Activity implements BeaconConsumer { * protected static final String TAG = "RangingActivity"; * private BeaconManager beaconManager = BeaconManager.getInstanceForApplication(this); * {@literal @}Override * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.activity_ranging); * beaconManager.bind(this); * } * {@literal @}Override * protected void onDestroy() { * super.onDestroy(); * beaconManager.unbind(this); * } * {@literal @}Override * public void onBeaconServiceConnect() { * beaconManager.setRangeNotifier(new RangeNotifier() { * {@literal @}Override * public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) { * if (beacons.size() > 0) { * Log.i(TAG, "The first beacon I see is about "+beacons.iterator().next().getDistance()+" meters away."); * } * } * }); * * try { * beaconManager.startRangingBeaconsInRegion(new Region("myRangingUniqueId", null, null, null)); * } catch (RemoteException e) { * e.printStackTrace(); * } * } * } * </code></pre> * * @author David G. Young * @author Andrew Reitz <[email protected]> */ public class BeaconManager { @NonNull private static final String TAG = "BeaconManager"; @NonNull private final Context mContext; @Nullable protected static volatile BeaconManager sInstance = null; @NonNull private final ConcurrentMap<BeaconConsumer, ConsumerInfo> consumers = new ConcurrentHashMap<>(); @Nullable private Messenger serviceMessenger = null; @NonNull protected final Set<RangeNotifier> rangeNotifiers = new CopyOnWriteArraySet<>(); @Nullable protected RangeNotifier dataRequestNotifier = null; @NonNull protected final Set<MonitorNotifier> monitorNotifiers = new CopyOnWriteArraySet<>(); @NonNull private final ArrayList<Region> rangedRegions = new ArrayList<>(); @NonNull private final List<BeaconParser> beaconParsers = new CopyOnWriteArrayList<>(); @Nullable private NonBeaconLeScanCallback mNonBeaconLeScanCallback; private boolean mRegionStatePersistenceEnabled = true; private boolean mBackgroundMode = false; private boolean mBackgroundModeUninitialized = true; private boolean mMainProcess = false; @Nullable private Boolean mScannerInSameProcess = null; private boolean mScheduledScanJobsEnabled = false; private static boolean sAndroidLScanningDisabled = false; private static boolean sManifestCheckingDisabled = false; @Nullable private Notification mForegroundServiceNotification = null; private int mForegroundServiceNotificationId = -1; /** * Private lock object for singleton initialization protecting against denial-of-service attack. */ private static final Object SINGLETON_LOCK = new Object(); /** * Set to true if you want to show library debugging. * * @param debug True turn on all logs for this library to be printed out to logcat. False turns * off detailed logging.. * * This is a convenience method that calls setLogger to a verbose logger and enables verbose * logging. For more fine grained control, use: * {@link org.altbeacon.beacon.logging.LogManager#setLogger(org.altbeacon.beacon.logging.Logger)} * instead. */ public static void setDebug(boolean debug) { if (debug) { LogManager.setLogger(Loggers.verboseLogger()); LogManager.setVerboseLoggingEnabled(true); } else { LogManager.setLogger(Loggers.empty()); LogManager.setVerboseLoggingEnabled(false); } } /** * The default duration in milliseconds of the Bluetooth scan cycle */ public static final long DEFAULT_FOREGROUND_SCAN_PERIOD = 1100; /** * The default duration in milliseconds spent not scanning between each Bluetooth scan cycle */ public static final long DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD = 0; /** * The default duration in milliseconds of the Bluetooth scan cycle when no ranging/monitoring clients are in the foreground */ public static final long DEFAULT_BACKGROUND_SCAN_PERIOD = 10000; /** * The default duration in milliseconds spent not scanning between each Bluetooth scan cycle when no ranging/monitoring clients are in the foreground */ public static final long DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD = 5 * 60 * 1000; /** * The default duration in milliseconds of region exit time */ public static final long DEFAULT_EXIT_PERIOD = 10000L; private static long sExitRegionPeriod = DEFAULT_EXIT_PERIOD; private long foregroundScanPeriod = DEFAULT_FOREGROUND_SCAN_PERIOD; private long foregroundBetweenScanPeriod = DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD; private long backgroundScanPeriod = DEFAULT_BACKGROUND_SCAN_PERIOD; private long backgroundBetweenScanPeriod = DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD; /** * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons. * This function is used to setup the period before calling {@link #bind} or when switching * between background/foreground. To have it effect on an already running scan (when the next * cycle starts), call {@link #updateScanPeriods} * * @param p */ public void setForegroundScanPeriod(long p) { foregroundScanPeriod = p; } /** * Sets the duration in milliseconds between each Bluetooth LE scan cycle to look for beacons. * This function is used to setup the period before calling {@link #bind} or when switching * between background/foreground. To have it effect on an already running scan (when the next * cycle starts), call {@link #updateScanPeriods} * * @param p */ public void setForegroundBetweenScanPeriod(long p) { foregroundBetweenScanPeriod = p; } /** * Sets the duration in milliseconds of each Bluetooth LE scan cycle to look for beacons. * This function is used to setup the period before calling {@link #bind} or when switching * between background/foreground. To have it effect on an already running scan (when the next * cycle starts), call {@link #updateScanPeriods} * * @param p */ public void setBackgroundScanPeriod(long p) { backgroundScanPeriod = p; } /** * Sets the duration in milliseconds spent not scanning between each Bluetooth LE scan cycle when no ranging/monitoring clients are in the foreground * * @param p */ public void setBackgroundBetweenScanPeriod(long p) { backgroundBetweenScanPeriod = p; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && backgroundBetweenScanPeriod < 15*60*1000 /* 15 min */) { LogManager.w(TAG, "Setting a short backgroundBetweenScanPeriod has no effect on "+ "Android 8+, which is limited to scanning every ~15 minutes"); } } /** * Set region exit period in milliseconds * * @param regionExitPeriod */ public static void setRegionExitPeriod(long regionExitPeriod){ sExitRegionPeriod = regionExitPeriod; BeaconManager instance = sInstance; if (instance != null) { instance.applySettings(); } } /** * Get region exit milliseconds * * @return exit region period in milliseconds */ public static long getRegionExitPeriod(){ return sExitRegionPeriod; } /** * An accessor for the singleton instance of this class. A context must be provided, but if you need to use it from a non-Activity * or non-Service class, you can attach it to another singleton or a subclass of the Android Application class. */ @NonNull public static BeaconManager getInstanceForApplication(@NonNull Context context) { /* * Follow double check pattern from Effective Java v2 Item 71. * * Bloch recommends using the local variable for this for performance reasons: * * > What this variable does is ensure that `field` is read only once in the common case * > where it's already initialized. While not strictly necessary, this may improve * > performance and is more elegant by the standards applied to low-level concurrent * > programming. On my machine, [this] is about 25 percent faster than the obvious * > version without a local variable. * * Joshua Bloch. Effective Java, Second Edition. Addison-Wesley, 2008. pages 283-284 */ BeaconManager instance = sInstance; if (instance == null) { synchronized (SINGLETON_LOCK) { instance = sInstance; if (instance == null) { sInstance = instance = new BeaconManager(context); } } } return instance; } protected BeaconManager(@NonNull Context context) { mContext = context.getApplicationContext(); checkIfMainProcess(); if (!sManifestCheckingDisabled) { verifyServiceDeclaration(); } this.beaconParsers.add(new AltBeaconParser()); setScheduledScanJobsEnabledDefault(); } /*** * Determines if this BeaconManager instance is associated with the main application process that * hosts the user interface. This is normally true unless the scanning service or another servide * is running in a separate process. * @return */ public boolean isMainProcess() { return mMainProcess; } /** * * Determines if this BeaconManager instance is not part of the process hosting the beacon scanning * service. This is normally false, except when scanning is hosted in a different process. * This will always return false until the scanning service starts up, at which time it will be * known if it is in a different process. * * @return */ public boolean isScannerInDifferentProcess() { // may be null if service not started yet, so explicitly check return mScannerInSameProcess != null && !mScannerInSameProcess; } /** * Reserved for internal use by the library. * @hide */ public void setScannerInSameProcess(boolean isScanner) { mScannerInSameProcess = isScanner; } protected void checkIfMainProcess() { ProcessUtils processUtils = new ProcessUtils(mContext); String processName = processUtils.getProcessName(); String packageName = processUtils.getPackageName(); int pid = processUtils.getPid(); mMainProcess = processUtils.isMainProcess(); LogManager.i(TAG, "BeaconManager started up on pid "+pid+" named '"+processName+"' for application package '"+packageName+"'. isMainProcess="+mMainProcess); } /** * Gets a list of the active beaconParsers. * * @return list of active BeaconParsers */ @NonNull public List<BeaconParser> getBeaconParsers() { return beaconParsers; } /** * Check if Bluetooth LE is supported by this Android device, and if so, make sure it is enabled. * * @return false if it is supported and not enabled * @throws BleNotAvailableException if Bluetooth LE is not supported. (Note: The Android emulator will do this) */ @TargetApi(18) public boolean checkAvailability() throws BleNotAvailableException { if (!isBleAvailableOrSimulated()) { throw new BleNotAvailableException("Bluetooth LE not supported by this device"); } return ((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled(); } /** * Binds an Android <code>Activity</code> or <code>Service</code> to the <code>BeaconService</code>. The * <code>Activity</code> or <code>Service</code> must implement the <code>beaconConsumer</code> interface so * that it can get a callback when the service is ready to use. * * @param consumer the <code>Activity</code> or <code>Service</code> that will receive the callback when the service is ready. */ public void bind(@NonNull BeaconConsumer consumer) { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } synchronized (consumers) { ConsumerInfo newConsumerInfo = new ConsumerInfo(); ConsumerInfo alreadyBoundConsumerInfo = consumers.putIfAbsent(consumer, newConsumerInfo); if (alreadyBoundConsumerInfo != null) { LogManager.d(TAG, "This consumer is already bound"); } else { LogManager.d(TAG, "This consumer is not bound. Binding now: %s", consumer); if (mScheduledScanJobsEnabled) { LogManager.d(TAG, "Not starting beacon scanning service. Using scheduled jobs"); consumer.onBeaconServiceConnect(); } else { LogManager.d(TAG, "Binding to service"); Intent intent = new Intent(consumer.getApplicationContext(), BeaconService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.getForegroundServiceNotification() != null) { if (isAnyConsumerBound()) { LogManager.i(TAG, "Not starting foreground beacon scanning" + " service. A consumer is already bound, so it should be started"); } else { LogManager.i(TAG, "Starting foreground beacon scanning service."); mContext.startForegroundService(intent); } } else { } consumer.bindService(intent, newConsumerInfo.beaconServiceConnection, Context.BIND_AUTO_CREATE); } LogManager.d(TAG, "consumer count is now: %s", consumers.size()); } } } /** * Unbinds an Android <code>Activity</code> or <code>Service</code> to the <code>BeaconService</code>. This should * typically be called in the onDestroy() method. * * @param consumer the <code>Activity</code> or <code>Service</code> that no longer needs to use the service. */ public void unbind(@NonNull BeaconConsumer consumer) { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } synchronized (consumers) { if (consumers.containsKey(consumer)) { LogManager.d(TAG, "Unbinding"); if (mScheduledScanJobsEnabled) { LogManager.d(TAG, "Not unbinding from scanning service as we are using scan jobs."); } else { consumer.unbindService(consumers.get(consumer).beaconServiceConnection); } LogManager.d(TAG, "Before unbind, consumer count is "+consumers.size()); consumers.remove(consumer); LogManager.d(TAG, "After unbind, consumer count is "+consumers.size()); if (consumers.size() == 0) { // If this is the last consumer to disconnect, the service will exit // release the serviceMessenger. serviceMessenger = null; // If we are using scan jobs, we cancel the active scan job if (mScheduledScanJobsEnabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { LogManager.i(TAG, "Cancelling scheduled jobs after unbind of last consumer."); ScanJobScheduler.getInstance().cancelSchedule(mContext); } } } } else { LogManager.d(TAG, "This consumer is not bound to: %s", consumer); LogManager.d(TAG, "Bound consumers: "); Set<Map.Entry<BeaconConsumer, ConsumerInfo>> consumers = this.consumers.entrySet(); for (Map.Entry<BeaconConsumer, ConsumerInfo> consumerEntry : consumers) { LogManager.d(TAG, String.valueOf(consumerEntry.getValue())); } } } } /** * Tells you if the passed beacon consumer is bound to the service * * @param consumer * @return */ public boolean isBound(@NonNull BeaconConsumer consumer) { synchronized(consumers) { // Annotation doesn't guarantee we get a non-null, but raising an NPE here is excessive //noinspection ConstantConditions return consumer != null && consumers.get(consumer) != null && (mScheduledScanJobsEnabled || serviceMessenger != null); } } /** * Tells you if the any beacon consumer is bound to the service * * @return */ public boolean isAnyConsumerBound() { synchronized(consumers) { return !consumers.isEmpty() && (mScheduledScanJobsEnabled || serviceMessenger != null); } } /** * This method notifies the beacon service that the application is either moving to background * mode or foreground mode. When in background mode, BluetoothLE scans to look for beacons are * executed less frequently in order to save battery life. The specific scan rates for * background and foreground operation are set by the defaults below, but may be customized. * When ranging in the background, the time between updates will be much less frequent than in * the foreground. Updates will come every time interval equal to the sum total of the * BackgroundScanPeriod and the BackgroundBetweenScanPeriod. * * @param backgroundMode true indicates the app is in the background * @see #DEFAULT_FOREGROUND_SCAN_PERIOD * @see #DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD; * @see #DEFAULT_BACKGROUND_SCAN_PERIOD; * @see #DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD; * @see #setForegroundScanPeriod(long p) * @see #setForegroundBetweenScanPeriod(long p) * @see #setBackgroundScanPeriod(long p) * @see #setBackgroundBetweenScanPeriod(long p) */ public void setBackgroundMode(boolean backgroundMode) { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } mBackgroundModeUninitialized = false; if (backgroundMode != mBackgroundMode) { mBackgroundMode = backgroundMode; try { this.updateScanPeriods(); } catch (RemoteException e) { LogManager.e(TAG, "Cannot contact service to set scan periods"); } } } /** * Configures using a `ScanJob` run with the `JobScheduler` to perform scans rather than using a * long-running `BeaconService` to do so. * * Calling with true on devices older than Android L (5.0) will not apply the change * as the JobScheduler is not available. * * This value defaults to true on Android O+ and false on devices with older OS versions. * Accepting the default value of false is recommended on Android N and earlier because * otherwise beacon scans may be run only once every 15 minutes in the background, and no low * power scans may be performed between scanning cycles. * * Setting this value to false will disable ScanJobs when the app is run on Android 8+, which * can prohibit delivery of callbacks when the app is in the background unless the scanning * process is running in a foreground service. * * This method may only be called if bind() has not yet been called, otherwise an * `IllegalStateException` is thown. * * @param enabled */ public void setEnableScheduledScanJobs(boolean enabled) { if (isAnyConsumerBound()) { LogManager.e(TAG, "ScanJob may not be configured because a consumer is" + " already bound."); throw new IllegalStateException("Method must be called before calling bind()"); } if (enabled && android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { LogManager.e(TAG, "ScanJob may not be configured because JobScheduler is not" + " availble prior to Android 5.0"); return; } if (!enabled && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { LogManager.w(TAG, "Disabling ScanJobs on Android 8+ may disable delivery of "+ "beacon callbacks in the background unless a foreground service is active."); } if(!enabled && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ScanJobScheduler.getInstance().cancelSchedule(mContext); } mScheduledScanJobsEnabled = enabled; } public boolean getScheduledScanJobsEnabled() { return mScheduledScanJobsEnabled; } public boolean getBackgroundMode() { return mBackgroundMode; } public long getBackgroundScanPeriod() { return backgroundScanPeriod; } public long getBackgroundBetweenScanPeriod() { return backgroundBetweenScanPeriod; } public long getForegroundScanPeriod() { return foregroundScanPeriod; } public long getForegroundBetweenScanPeriod() { return foregroundBetweenScanPeriod; } /** * @return indicator of whether any calls have yet been made to set the * background mode */ public boolean isBackgroundModeUninitialized() { return mBackgroundModeUninitialized; } /** * Specifies a class that should be called each time the <code>BeaconService</code> gets ranging * data, which is nominally once per second when beacons are detected. * <p/> * IMPORTANT: Only one RangeNotifier may be active for a given application. If two different * activities or services set different RangeNotifier instances, the last one set will receive * all the notifications. * * @param notifier The {@link RangeNotifier} to register. * @see RangeNotifier * @deprecated replaced by (@link #addRangeNotifier) */ @Deprecated public void setRangeNotifier(@Nullable RangeNotifier notifier) { rangeNotifiers.clear(); if (null != notifier) { addRangeNotifier(notifier); } } /** * Specifies a class that should be called each time the <code>BeaconService</code> gets ranging * data, which is nominally once per second when beacons are detected. * <p/> * Permits to register several <code>RangeNotifier</code> objects. * <p/> * The notifier must be unregistered using (@link #removeRangeNotifier) * * @param notifier The {@link RangeNotifier} to register. * @see RangeNotifier */ public void addRangeNotifier(@NonNull RangeNotifier notifier) { //noinspection ConstantConditions if (notifier != null) { rangeNotifiers.add(notifier); } } /** * Specifies a class to remove from the array of <code>RangeNotifier</code> * * @param notifier The {@link RangeNotifier} to unregister. * @see RangeNotifier */ public boolean removeRangeNotifier(@NonNull RangeNotifier notifier) { return rangeNotifiers.remove(notifier); } /** * Remove all the Range Notifiers. */ public void removeAllRangeNotifiers() { rangeNotifiers.clear(); } /** * Specifies a class that should be called each time the <code>BeaconService</code> sees * or stops seeing a Region of beacons. * <p/> * IMPORTANT: Only one MonitorNotifier may be active for a given application. If two different * activities or services set different MonitorNotifier instances, the last one set will receive * all the notifications. * * @param notifier The {@link MonitorNotifier} to register. * @see MonitorNotifier * @see #startMonitoringBeaconsInRegion(Region) * @see Region * @deprecated replaced by {@link #addMonitorNotifier} */ @Deprecated public void setMonitorNotifier(@Nullable MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return; } monitorNotifiers.clear(); if (null != notifier) { addMonitorNotifier(notifier); } } /** * Specifies a class that should be called each time the <code>BeaconService</code> sees or * stops seeing a Region of beacons. * <p/> * Permits to register several <code>MonitorNotifier</code> objects. * <p/> * Unregister the notifier using {@link #removeMonitoreNotifier} * * @param notifier The {@link MonitorNotifier} to register. * @see MonitorNotifier * @see #startMonitoringBeaconsInRegion(Region) * @see Region */ public void addMonitorNotifier(@NonNull MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return; } //noinspection ConstantConditions if (notifier != null) { monitorNotifiers.add(notifier); } } /** * @see #removeMonitorNotifier * @deprecated Misspelled. Replaced by {@link #removeMonitorNotifier} */ @Deprecated public boolean removeMonitoreNotifier(@NonNull MonitorNotifier notifier) { return removeMonitorNotifier(notifier); } /** * Specifies a class to remove from the array of <code>MonitorNotifier</code>. * * @param notifier The {@link MonitorNotifier} to unregister. * @see MonitorNotifier * @see #startMonitoringBeaconsInRegion(Region) * @see Region */ public boolean removeMonitorNotifier(@NonNull MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return false; } return monitorNotifiers.remove(notifier); } /** * Remove all the Monitor Notifiers. */ public void removeAllMonitorNotifiers() { if (determineIfCalledFromSeparateScannerProcess()) { return; } monitorNotifiers.clear(); } /** * @see #setRegionStatePersistenceEnabled * @deprecated Misspelled. Replaced by {@link #setRegionStatePersistenceEnabled} */ @Deprecated public void setRegionStatePeristenceEnabled(boolean enabled) { setRegionStatePersistenceEnabled(enabled); } /** * Turns off saving the state of monitored regions to persistent storage so it is retained over * app restarts. Defaults to enabled. When enabled, there will not be an "extra" region entry * event when the app starts up and a beacon for a monitored region was previously visible * within the past 15 minutes. Note that there is a limit to 50 monitored regions that may be * persisted. If more than 50 regions are monitored, state is not persisted for any. * * @param enabled true to enable the region state persistence, false to disable it. */ public void setRegionStatePersistenceEnabled(boolean enabled) { mRegionStatePersistenceEnabled = enabled; if (!isScannerInDifferentProcess()) { if (enabled) { MonitoringStatus.getInstanceForApplication(mContext).startStatusPreservation(); } else { MonitoringStatus.getInstanceForApplication(mContext).stopStatusPreservation(); } } this.applySettings(); } /** * Indicates whether region state preservation is enabled * @return */ public boolean isRegionStatePersistenceEnabled() { return mRegionStatePersistenceEnabled; } /** * Requests the current in/out state on the specified region. If the region is being monitored, * this will cause an asynchronous callback on the `MonitorNotifier`'s `didDetermineStateForRegion` * method. If it is not a monitored region, it will be ignored. * @param region */ public void requestStateForRegion(@NonNull Region region) { if (determineIfCalledFromSeparateScannerProcess()) { return; } MonitoringStatus status = MonitoringStatus.getInstanceForApplication(mContext); RegionMonitoringState stateObj = status.stateOf(region); int state = MonitorNotifier.OUTSIDE; if (stateObj != null && stateObj.getInside()) { state = MonitorNotifier.INSIDE; } for (MonitorNotifier notifier : monitorNotifiers) { notifier.didDetermineStateForRegion(state, region); } } /** * Tells the <code>BeaconService</code> to start looking for beacons that match the passed * <code>Region</code> object, and providing updates on the estimated mDistance every seconds while * beacons in the Region are visible. Note that the Region's unique identifier must be retained to * later call the stopRangingBeaconsInRegion method. * * @param region * @see BeaconManager#setRangeNotifier(RangeNotifier) * @see BeaconManager#stopRangingBeaconsInRegion(Region region) * @see RangeNotifier * @see Region */ @TargetApi(18) public void startRangingBeaconsInRegion(@NonNull Region region) throws RemoteException { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } synchronized (rangedRegions) { rangedRegions.add(region); } applyChangesToServices(BeaconService.MSG_START_RANGING, region); } /** * Tells the <code>BeaconService</code> to stop looking for beacons that match the passed * <code>Region</code> object and providing mDistance information for them. * * @param region * @see #setMonitorNotifier(MonitorNotifier notifier) * @see #startMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region */ @TargetApi(18) public void stopRangingBeaconsInRegion(@NonNull Region region) throws RemoteException { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } synchronized (rangedRegions) { Region regionToRemove = null; for (Region rangedRegion : rangedRegions) { if (region.getUniqueId().equals(rangedRegion.getUniqueId())) { regionToRemove = rangedRegion; } } rangedRegions.remove(regionToRemove); } applyChangesToServices(BeaconService.MSG_STOP_RANGING, region); } /** * Call this method if you are running the scanner service in a different process in order to * synchronize any configuration settings, including BeaconParsers to the scanner * @see #isScannerInDifferentProcess() */ public void applySettings() { if (determineIfCalledFromSeparateScannerProcess()) { return; } if (!isAnyConsumerBound()) { LogManager.d(TAG, "Not synchronizing settings to service, as it has not started up yet"); } else if (isScannerInDifferentProcess()) { LogManager.d(TAG, "Synchronizing settings to service"); syncSettingsToService(); } else { LogManager.d(TAG, "Not synchronizing settings to service, as it is in the same process"); } } protected void syncSettingsToService() { if (mScheduledScanJobsEnabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ScanJobScheduler.getInstance().applySettingsToScheduledJob(mContext, this); } return; } try { applyChangesToServices(BeaconService.MSG_SYNC_SETTINGS, null); } catch (RemoteException e) { LogManager.e(TAG, "Failed to sync settings to service", e); } } /** * Tells the <code>BeaconService</code> to start looking for beacons that match the passed * <code>Region</code> object. Note that the Region's unique identifier must be retained to * later call the stopMonitoringBeaconsInRegion method. * * @param region * @see BeaconManager#setMonitorNotifier(MonitorNotifier) * @see BeaconManager#stopMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region */ @TargetApi(18) public void startMonitoringBeaconsInRegion(@NonNull Region region) throws RemoteException { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (mScheduledScanJobsEnabled) { MonitoringStatus.getInstanceForApplication(mContext).addRegion(region, new Callback(callbackPackageName())); } applyChangesToServices(BeaconService.MSG_START_MONITORING, region); if (isScannerInDifferentProcess()) { MonitoringStatus.getInstanceForApplication(mContext).addLocalRegion(region); } this.requestStateForRegion(region); } /** * Tells the <code>BeaconService</code> to stop looking for beacons that match the passed * <code>Region</code> object. Note that the Region's unique identifier is used to match it to * an existing monitored Region. * * @param region * @see BeaconManager#setMonitorNotifier(MonitorNotifier) * @see BeaconManager#startMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region */ @TargetApi(18) public void stopMonitoringBeaconsInRegion(@NonNull Region region) throws RemoteException { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (mScheduledScanJobsEnabled) { MonitoringStatus.getInstanceForApplication(mContext).removeRegion(region); } applyChangesToServices(BeaconService.MSG_STOP_MONITORING, region); if (isScannerInDifferentProcess()) { MonitoringStatus.getInstanceForApplication(mContext).removeLocalRegion(region); } } /** * Updates an already running scan with scanPeriod/betweenScanPeriod according to Background/Foreground state. * Change will take effect on the start of the next scan cycle. * * @throws RemoteException - If the BeaconManager is not bound to the service. */ @TargetApi(18) public void updateScanPeriods() throws RemoteException { if (!isBleAvailableOrSimulated()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } LogManager.d(TAG, "updating background flag to %s", mBackgroundMode); LogManager.d(TAG, "updating scan period to %s, %s", this.getScanPeriod(), this.getBetweenScanPeriod()); applyChangesToServices(BeaconService.MSG_SET_SCAN_PERIODS, null); } @TargetApi(18) private void applyChangesToServices(int type, Region region) throws RemoteException { if (!isAnyConsumerBound()) { LogManager.w(TAG, "The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); return; } if (mScheduledScanJobsEnabled) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ScanJobScheduler.getInstance().applySettingsToScheduledJob(mContext, this); } return; } Message msg = Message.obtain(null, type, 0, 0); if (type == BeaconService.MSG_SET_SCAN_PERIODS) { msg.setData(new StartRMData(this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode).toBundle()); } else if (type == BeaconService.MSG_SYNC_SETTINGS) { msg.setData(new SettingsData().collect(mContext).toBundle()); } else { msg.setData(new StartRMData(region, callbackPackageName(), getScanPeriod(), getBetweenScanPeriod(), mBackgroundMode).toBundle()); } serviceMessenger.send(msg); } private String callbackPackageName() { String packageName = mContext.getPackageName(); LogManager.d(TAG, "callback packageName: %s", packageName); return packageName; } /** * @return the first registered monitorNotifier * @deprecated replaced by (@link #getMonitorNotifiers) */ @Deprecated @Nullable public MonitorNotifier getMonitoringNotifier() { Iterator<MonitorNotifier> iterator = monitorNotifiers.iterator(); if (iterator.hasNext()) { return iterator.next(); } return null; } /** * Read-only access to the registered {@link MonitorNotifier} instances * <p> * This provides a thread-safe "read-only" view of the {@link Set} of registered monitor * notifiers. Attempts to modify the returned set, or its iterator, will throw an * {@link UnsupportedOperationException}. Modifications to the underlying set should be made * through {@link #addMonitorNotifier(MonitorNotifier)} and * {@link #removeMonitorNotifier(MonitorNotifier)}. * * @return a thread-safe {@linkplain Collections#unmodifiableSet(Set) unmodifiable view} * providing "read-only" access to the registered {@link MonitorNotifier} instances * @see #addMonitorNotifier(MonitorNotifier) * @see #removeMonitorNotifier(MonitorNotifier) * @see Collections#unmodifiableSet(Set) */ @NonNull public Set<MonitorNotifier> getMonitoringNotifiers(){ return Collections.unmodifiableSet(monitorNotifiers); } /** * @return the first registered rangeNotifier * @deprecated replaced by (@link #getRangeNotifiers) */ @Deprecated @Nullable public RangeNotifier getRangingNotifier() { Iterator<RangeNotifier> iterator = rangeNotifiers.iterator(); if (iterator.hasNext()) { return iterator.next(); } return null; } /** * Read-only access to the registered {@link RangeNotifier} instances * <p> * This provides a thread-safe "read-only" view of the {@link Set} of registered range * notifiers. Attempts to modify the returned set, or its iterator, will throw an * {@link UnsupportedOperationException}. Modifications to the underlying set should be made * through {@link #addRangeNotifier(RangeNotifier)} and * {@link #removeRangeNotifier(RangeNotifier)}. * * @return a thread-safe {@linkplain Collections#unmodifiableSet(Set) unmodifiable view} * providing "read-only" access to the registered {@link RangeNotifier} instances * @see #addRangeNotifier(RangeNotifier) * @see #removeRangeNotifier(RangeNotifier) * @see Collections#unmodifiableSet(Set) */ @NonNull public Set<RangeNotifier> getRangingNotifiers() { return Collections.unmodifiableSet(rangeNotifiers); } /** * @return the list of regions currently being monitored */ @NonNull public Collection<Region> getMonitoredRegions() { return MonitoringStatus.getInstanceForApplication(mContext).regions(); } /** * @return the list of regions currently being ranged */ @NonNull public Collection<Region> getRangedRegions() { synchronized(this.rangedRegions) { return new ArrayList<>(this.rangedRegions); } } /** * Convenience method for logging debug by the library * * @param tag * @param message * @deprecated This will be removed in a later release. Use * {@link org.altbeacon.beacon.logging.LogManager#d(String, String, Object...)} instead. */ @Deprecated public static void logDebug(String tag, String message) { LogManager.d(tag, message); } /** * Convenience method for logging debug by the library * * @param tag * @param message * @param t * @deprecated This will be removed in a later release. Use * {@link org.altbeacon.beacon.logging.LogManager#d(Throwable, String, String, Object...)} * instead. */ @Deprecated public static void logDebug(String tag, String message, Throwable t) { LogManager.d(t, tag, message); } @Nullable protected static BeaconSimulator beaconSimulator; protected static String distanceModelUpdateUrl = "https://s3.amazonaws.com/android-beacon-library/android-distance.json"; public static String getDistanceModelUpdateUrl() { return distanceModelUpdateUrl; } public static void setDistanceModelUpdateUrl(@NonNull String url) { warnIfScannerNotInSameProcess(); distanceModelUpdateUrl = url; } /** * Default class for rssi filter/calculation implementation */ protected static Class rssiFilterImplClass = RunningAverageRssiFilter.class; public static void setRssiFilterImplClass(@NonNull Class c) { warnIfScannerNotInSameProcess(); rssiFilterImplClass = c; } public static Class getRssiFilterImplClass() { return rssiFilterImplClass; } /** * Allow the library to use a tracking cache * @param useTrackingCache */ public static void setUseTrackingCache(boolean useTrackingCache) { RangeState.setUseTrackingCache(useTrackingCache); if (sInstance != null) { sInstance.applySettings(); } } /** * Set the period of time, in which a beacon did not receive new * measurements * @param maxTrackingAge in milliseconds */ public void setMaxTrackingAge(int maxTrackingAge) { RangedBeacon.setMaxTrackinAge(maxTrackingAge); } public static void setBeaconSimulator(BeaconSimulator beaconSimulator) { warnIfScannerNotInSameProcess(); BeaconManager.beaconSimulator = beaconSimulator; } @Nullable public static BeaconSimulator getBeaconSimulator() { return BeaconManager.beaconSimulator; } protected void setDataRequestNotifier(@Nullable RangeNotifier notifier) { this.dataRequestNotifier = notifier; } @Nullable protected RangeNotifier getDataRequestNotifier() { return this.dataRequestNotifier; } @Nullable public NonBeaconLeScanCallback getNonBeaconLeScanCallback() { return mNonBeaconLeScanCallback; } public void setNonBeaconLeScanCallback(@Nullable NonBeaconLeScanCallback callback) { mNonBeaconLeScanCallback = callback; } private boolean isBleAvailableOrSimulated() { if (getBeaconSimulator() != null) { return true; } return isBleAvailable(); } private boolean isBleAvailable() { boolean available = false; if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) { LogManager.w(TAG, "Bluetooth LE not supported prior to API 18."); } else if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { LogManager.w(TAG, "This device does not support bluetooth LE."); } else { available = true; } return available; } private long getScanPeriod() { if (mBackgroundMode) { return backgroundScanPeriod; } else { return foregroundScanPeriod; } } private long getBetweenScanPeriod() { if (mBackgroundMode) { return backgroundBetweenScanPeriod; } else { return foregroundBetweenScanPeriod; } } private void verifyServiceDeclaration() { final PackageManager packageManager = mContext.getPackageManager(); final Intent intent = new Intent(mContext, BeaconService.class); List<ResolveInfo> resolveInfo = packageManager.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY); if (resolveInfo != null && resolveInfo.isEmpty()) { throw new ServiceNotDeclaredException(); } } private class ConsumerInfo { public boolean isConnected = false; @NonNull public BeaconServiceConnection beaconServiceConnection; public ConsumerInfo() { this.isConnected = false; this.beaconServiceConnection= new BeaconServiceConnection(); } } private class BeaconServiceConnection implements ServiceConnection { private BeaconServiceConnection() { } // Called when the connection with the service is established public void onServiceConnected(ComponentName className, IBinder service) { LogManager.d(TAG, "we have a connection to the service now"); if (mScannerInSameProcess == null) { mScannerInSameProcess = false; } serviceMessenger = new Messenger(service); // This will sync settings to the scanning service if it is in a different process applySettings(); synchronized(consumers) { Iterator<Map.Entry<BeaconConsumer, ConsumerInfo>> iter = consumers.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<BeaconConsumer, ConsumerInfo> entry = iter.next(); if (!entry.getValue().isConnected) { entry.getKey().onBeaconServiceConnect(); entry.getValue().isConnected = true; } } } } // Called when the connection with the service disconnects public void onServiceDisconnected(ComponentName className) { LogManager.e(TAG, "onServiceDisconnected"); serviceMessenger = null; } } public class ServiceNotDeclaredException extends RuntimeException { public ServiceNotDeclaredException() { super("The BeaconService is not properly declared in AndroidManifest.xml. If using Eclipse," + " please verify that your project.properties has manifestmerger.enabled=true"); } } /** * Determines if Android L Scanning is disabled by user selection * * @return */ public static boolean isAndroidLScanningDisabled() { return sAndroidLScanningDisabled; } /** * Allows disabling use of Android L BLE Scanning APIs on devices with API 21+ * If set to false (default), devices with API 21+ will use the Android L APIs to * scan for beacons * * @param disabled */ public static void setAndroidLScanningDisabled(boolean disabled) { sAndroidLScanningDisabled = disabled; BeaconManager instance = sInstance; if (instance != null) { instance.applySettings(); } } /** * Deprecated misspelled method * @see #setManifestCheckingDisabled(boolean) * @param disabled */ @Deprecated public static void setsManifestCheckingDisabled(boolean disabled) { sManifestCheckingDisabled = disabled; } /** * Allows disabling check of manifest for proper configuration of service. Useful for unit * testing * * @param disabled */ public static void setManifestCheckingDisabled(boolean disabled) { sManifestCheckingDisabled = disabled; } /** * Returns whether manifest checking is disabled */ public static boolean getManifestCheckingDisabled() { return sManifestCheckingDisabled; } /** * Configures the library to use a foreground service for bacon scanning. This allows nearly * constant scanning on most Android versions to get around background limits, and displays an * icon to the user to indicate that the app is doing something in the background, even on * Android 8+. This will disable the user of the JobScheduler on Android 8 to do scans. Note * that this method does not by itself enable constant scanning. The scan intervals will work * as normal and must be configurd to specific values depending on how often you wish to scan. * * @see #setForegroundScanPeriod(long) * @see #setForegroundBetweenScanPeriod(long) * * This method requires a notification to display a message to the user about why the app is * scanning in the background. The notification must include an icon that will be displayed * in the top bar whenever the scanning service is running. * * If the BeaconService is configured to run in a different process, this call will have no * effect. * * @param notification - the notification that will be displayed when beacon scanning is active, * along with the icon that shows up in the status bar. * * @throws IllegalStateException if called after consumers are already bound to the scanning * service */ public void enableForegroundServiceScanning(Notification notification, int notificationId) throws IllegalStateException { if (isAnyConsumerBound()) { throw new IllegalStateException("May not be called after consumers are already bound."); } if (notification == null) { throw new NullPointerException("Notification cannot be null"); } setEnableScheduledScanJobs(false); mForegroundServiceNotification = notification; mForegroundServiceNotificationId = notificationId; } /** * Disables a foreground scanning service, if previously configured. * * @see #enableForegroundServiceScanning * * In order to call this method to disable a foreground service, you must unbind from the * BeaconManager. You can then rebind after this call is made. * * @throws IllegalStateException if called after consumers are already bound to the scanning * service */ public void disableForegroundServiceScanning() throws IllegalStateException { if (isAnyConsumerBound()) { throw new IllegalStateException("May not be called after consumers are already bound"); } mForegroundServiceNotification = null; setScheduledScanJobsEnabledDefault(); } /** * @see #enableForegroundServiceScanning * @return The notification shown for the beacon scanning service, if so configured */ public Notification getForegroundServiceNotification() { return mForegroundServiceNotification; } /** * @see #enableForegroundServiceScanning * @return The notification shown for the beacon scanning service, if so configured */ public int getForegroundServiceNotificationId() { return mForegroundServiceNotificationId; } private boolean determineIfCalledFromSeparateScannerProcess() { if (isScannerInDifferentProcess() && !isMainProcess()) { LogManager.w(TAG, "Ranging/Monitoring may not be controlled from a separate "+ "BeaconScanner process. To remove this warning, please wrap this call in:"+ " if (beaconManager.isMainProcess())"); return true; } return false; } private static void warnIfScannerNotInSameProcess() { BeaconManager instance = sInstance; if (instance != null && instance.isScannerInDifferentProcess()) { LogManager.w(TAG, "Unsupported configuration change made for BeaconScanner in separate process"); } } private void setScheduledScanJobsEnabledDefault() { mScheduledScanJobsEnabled = android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } }