/** * 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.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.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import org.altbeacon.beacon.logging.LogManager; import org.altbeacon.beacon.logging.Loggers; import org.altbeacon.beacon.service.BeaconService; 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.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.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 <andrew@andrewreitz.com> */ public class BeaconManager { private static final String TAG = "BeaconManager"; private Context mContext; protected static volatile BeaconManager sInstance = null; private final ConcurrentMap<BeaconConsumer, ConsumerInfo> consumers = new ConcurrentHashMap<BeaconConsumer,ConsumerInfo>(); private Messenger serviceMessenger = null; protected final Set<RangeNotifier> rangeNotifiers = new CopyOnWriteArraySet<>(); protected RangeNotifier dataRequestNotifier = null; protected final Set<MonitorNotifier> monitorNotifiers = new CopyOnWriteArraySet<>(); private final ArrayList<Region> rangedRegions = new ArrayList<Region>(); private final List<BeaconParser> beaconParsers = new CopyOnWriteArrayList<>(); private NonBeaconLeScanCallback mNonBeaconLeScanCallback; private boolean mRegionStatePersistenceEnabled = true; private boolean mBackgroundMode = false; private boolean mBackgroundModeUninitialized = true; private boolean mMainProcess = false; private Boolean mScannerInSameProcess = null; private static boolean sAndroidLScanningDisabled = false; private static boolean sManifestCheckingDisabled = false; /** * 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 all logging. * @deprecated To be removed in a future release. Use * {@link org.altbeacon.beacon.logging.LogManager#setLogger(org.altbeacon.beacon.logging.Logger)} * instead. */ @Deprecated 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; } /** * Set region exit period in milliseconds * * @param regionExitPeriod */ public static void setRegionExitPeriod(long regionExitPeriod){ sExitRegionPeriod = regionExitPeriod; if (sInstance != null) { sInstance.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. */ public static BeaconManager getInstanceForApplication(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(Context context) { mContext = context.getApplicationContext(); checkIfMainProcess(); if (!sManifestCheckingDisabled) { verifyServiceDeclaration(); } this.beaconParsers.add(new AltBeaconParser()); } /*** * 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 */ 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 (!isBleAvailable()) { 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(BeaconConsumer consumer) { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { LogManager.w(TAG, "This device does not support bluetooth LE. Will not start beacon scanning."); 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: %s", consumer); Intent intent = new Intent(consumer.getApplicationContext(), BeaconService.class); 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(BeaconConsumer consumer) { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } synchronized (consumers) { if (consumers.containsKey(consumer)) { LogManager.d(TAG, "Unbinding"); consumer.unbindService(consumers.get(consumer).beaconServiceConnection); consumers.remove(consumer); if (consumers.size() == 0) { // If this is the last consumer to disconnect, the service will exit // release the serviceMessenger. serviceMessenger = null; // Reset the mBackgroundMode to false, which is the default value // This way when we restart ranging or monitoring it will always be in // foreground mode mBackgroundMode = false; } } 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(BeaconConsumer consumer) { synchronized(consumers) { return consumer != null && consumers.get(consumer) != null && (serviceMessenger != null); } } /** * Tells you if the any beacon consumer is bound to the service * * @return */ public boolean isAnyConsumerBound() { synchronized(consumers) { return consumers.size() > 0 && (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 (!isBleAvailable()) { 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"); } } } /** * @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(RangeNotifier notifier) { synchronized (rangeNotifiers) { rangeNotifiers.clear(); } 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(RangeNotifier notifier) { if (notifier != null) { synchronized (rangeNotifiers) { 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(RangeNotifier notifier) { synchronized (rangeNotifiers) { return rangeNotifiers.remove(notifier); } } /** * Remove all the Range Notifiers. */ public void removeAllRangeNotifiers() { synchronized (rangeNotifiers) { 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(MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return; } synchronized (monitorNotifiers) { monitorNotifiers.clear(); } 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(MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return; } if (notifier != null) { synchronized (monitorNotifiers) { monitorNotifiers.add(notifier); } } } /** * @see #removeMonitorNotifier * @deprecated Misspelled. Replaced by {@link #removeMonitorNotifier} */ @Deprecated public boolean removeMonitoreNotifier(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(MonitorNotifier notifier) { if (determineIfCalledFromSeparateScannerProcess()) { return false; } synchronized (monitorNotifiers) { return monitorNotifiers.remove(notifier); } } /** * Remove all the Monitor Notifiers. */ public void removeAllMonitorNotifiers() { if (determineIfCalledFromSeparateScannerProcess()) { return; } synchronized (monitorNotifiers) { 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(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; } synchronized (monitorNotifiers) { 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(Region region) throws RemoteException { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (serviceMessenger == null) { throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); } Message msg = Message.obtain(null, BeaconService.MSG_START_RANGING, 0, 0); msg.setData(new StartRMData(region, callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode).toBundle()); serviceMessenger.send(msg); synchronized (rangedRegions) { rangedRegions.add(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(Region region) throws RemoteException { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (serviceMessenger == null) { throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); } Message msg = Message.obtain(null, BeaconService.MSG_STOP_RANGING, 0, 0); msg.setData(new StartRMData(region, callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode).toBundle()); serviceMessenger.send(msg); synchronized (rangedRegions) { Region regionToRemove = null; for (Region rangedRegion : rangedRegions) { if (region.getUniqueId().equals(rangedRegion.getUniqueId())) { regionToRemove = rangedRegion; } } rangedRegions.remove(regionToRemove); } } /** * 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() && !isScannerInDifferentProcess() == false) { LogManager.d(TAG, "Synchronizing settings to service"); syncSettingsToService(); } else { if (isAnyConsumerBound() == false) { LogManager.d(TAG, "Not synchronizing settings to service, as it has not started up yet"); } else { LogManager.d(TAG, "Not synchronizing settings to service, as it is in the same process"); } } } protected void syncSettingsToService() { if (serviceMessenger == null) { LogManager.e(TAG, "The BeaconManager is not bound to the service. Settings synchronization will fail."); return; } try { Message msg = Message.obtain(null, BeaconService.MSG_SYNC_SETTINGS, 0, 0); msg.setData(new SettingsData().collect(mContext).toBundle()); serviceMessenger.send(msg); } 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(Region region) throws RemoteException { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (serviceMessenger == null) { throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); } LogManager.d(TAG, "Starting monitoring region "+region+" with uniqueID: "+region.getUniqueId()); Message msg = Message.obtain(null, BeaconService.MSG_START_MONITORING, 0, 0); msg.setData(new StartRMData(region, callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode).toBundle()); serviceMessenger.send(msg); 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(Region region) throws RemoteException { if (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (serviceMessenger == null) { throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); } Message msg = Message.obtain(null, BeaconService.MSG_STOP_MONITORING, 0, 0); msg.setData(new StartRMData(region, callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode).toBundle()); serviceMessenger.send(msg); 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 (!isBleAvailable()) { LogManager.w(TAG, "Method invocation will be ignored."); return; } if (determineIfCalledFromSeparateScannerProcess()) { return; } if (serviceMessenger == null) { throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()"); } Message msg = Message.obtain(null, BeaconService.MSG_SET_SCAN_PERIODS, 0, 0); LogManager.d(TAG, "updating background flag to %s", mBackgroundMode); LogManager.d(TAG, "updating scan period to %s, %s", this.getScanPeriod(), this.getBetweenScanPeriod()); msg.setData(new StartRMData(this.getScanPeriod(), this.getBetweenScanPeriod(), this.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 public MonitorNotifier getMonitoringNotifier() { synchronized (monitorNotifiers) { if (monitorNotifiers.size() > 0) { return monitorNotifiers.iterator().next(); } return null; } } /** * @return the list of registered monitorNotifier */ public Set<MonitorNotifier> getMonitoringNotifiers(){ return monitorNotifiers; } /** * @return the first registered rangeNotifier * @deprecated replaced by (@link #getRangeNotifiers) */ @Deprecated public RangeNotifier getRangingNotifier() { synchronized (rangeNotifiers) { if (rangeNotifiers.size() > 0) { return rangeNotifiers.iterator().next(); } return null; } } /** * @return the list of registered rangeNotifier */ public Set<RangeNotifier> getRangingNotifiers() { return rangeNotifiers; } /** * @return the list of regions currently being monitored */ public Collection<Region> getMonitoredRegions() { return MonitoringStatus.getInstanceForApplication(mContext).regions(); } /** * @return the list of regions currently being ranged */ public Collection<Region> getRangedRegions() { synchronized(this.rangedRegions) { return new ArrayList<Region>(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); } protected static BeaconSimulator beaconSimulator; protected static String distanceModelUpdateUrl = "http://data.altbeacon.org/android-distance.json"; public static String getDistanceModelUpdateUrl() { return distanceModelUpdateUrl; } public static void setDistanceModelUpdateUrl(String url) { warnIfScannerNotInSameProcess(); distanceModelUpdateUrl = url; } /** * Default class for rssi filter/calculation implementation */ protected static Class rssiFilterImplClass = RunningAverageRssiFilter.class; public static void setRssiFilterImplClass(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; } public static BeaconSimulator getBeaconSimulator() { return BeaconManager.beaconSimulator; } protected void setDataRequestNotifier(RangeNotifier notifier) { this.dataRequestNotifier = notifier; } protected RangeNotifier getDataRequestNotifier() { return this.dataRequestNotifier; } public NonBeaconLeScanCallback getNonBeaconLeScanCallback() { return mNonBeaconLeScanCallback; } public void setNonBeaconLeScanCallback(NonBeaconLeScanCallback callback) { mNonBeaconLeScanCallback = callback; } 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.size() == 0) { throw new ServiceNotDeclaredException(); } } private class ConsumerInfo { public boolean isConnected = false; 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; if (sInstance != null) { sInstance.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; } 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() { if (sInstance != null && sInstance.isScannerInDifferentProcess()) { LogManager.w(TAG, "Unsupported configuration change made for BeaconScanner in separate process"); } } }