/** * 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 com.radiusnetworks.ibeacon; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import com.radiusnetworks.ibeacon.simulator.BeaconSimulator; import com.radiusnetworks.ibeacon.service.IBeaconService; import com.radiusnetworks.ibeacon.service.RegionData; import com.radiusnetworks.ibeacon.service.StartRMData; 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.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; /** * An class used to set up interaction with iBeacons from an <code>Activity</code> or <code>Service</code>. * This class is used in conjunction with <code>IBeaconConsumer</code> interface, which provides a callback * when the <code>IBeaconService</code> is ready to use. Until this callback is made, ranging and monitoring * of iBeacons is not possible. * * In the example below, an Activity implements the <code>IBeaconConsumer</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 IBeaconConsumer { * protected static final String TAG = "RangingActivity"; * private IBeaconManager iBeaconManager = IBeaconManager.getInstanceForApplication(this); * {@literal @}Override * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.activity_ranging); * iBeaconManager.bind(this); * } * {@literal @}Override * protected void onDestroy() { * super.onDestroy(); * iBeaconManager.unBind(this); * } * {@literal @}Override * public void onIBeaconServiceConnect() { * iBeaconManager.setRangeNotifier(new RangeNotifier() { * {@literal @}Override * public void didRangeBeaconsInRegion(Collection<IBeacon> iBeacons, Region region) { * if (iBeacons.size() > 0) { * Log.i(TAG, "The first iBeacon I see is about "+iBeacons.iterator().next().getAccuracy()+" meters away."); * } * } * }); * * try { * iBeaconManager.startRangingBeaconsInRegion(new Region("myRangingUniqueId", null, null, null)); * } catch (RemoteException e) { } * } * } * </code></pre> * * @author David G. Young * */ @TargetApi(4) public class IBeaconManager { private static final String TAG = "IBeaconManager"; private Context context; protected static IBeaconManager client = null; private Map<IBeaconConsumer,ConsumerInfo> consumers = new HashMap<IBeaconConsumer,ConsumerInfo>(); private Messenger serviceMessenger = null; protected RangeNotifier rangeNotifier = null; protected RangeNotifier dataRequestNotifier = null; protected MonitorNotifier monitorNotifier = null; private ArrayList<Region> monitoredRegions = new ArrayList<Region>(); private ArrayList<Region> rangedRegions = new ArrayList<Region>(); /** * set to true if you want to see debug messages associated with this library */ public static boolean debug = false; public static void setDebug(boolean debug) { IBeaconManager.debug = debug; } /** * 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; 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 iBeacons. * 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 iBeacons. * 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 iBeacons. * 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; } /** * 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 IBeaconManager getInstanceForApplication(Context context) { if (client == null) { if (IBeaconManager.debug) Log.d(TAG, "IBeaconManager instance creation"); client = new IBeaconManager(context); } return client; } protected IBeaconManager(Context context) { this.context = context; } /** * Check if Bluetooth LE is supported by this Android device, and if so, make sure it is enabled. * @throws BleNotAvailableException if Bluetooth LE is not supported. (Note: The Android emulator will do this) * @return false if it is supported and not enabled */ @TargetApi(18) public boolean checkAvailability() throws BleNotAvailableException { if (android.os.Build.VERSION.SDK_INT < 18) { throw new BleNotAvailableException("Bluetooth LE not supported by this device"); } if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { throw new BleNotAvailableException("Bluetooth LE not supported by this device"); } else { if (((BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled()){ return true; } } return false; } /** * Binds an Android <code>Activity</code> or <code>Service</code> to the <code>IBeaconService</code>. The * <code>Activity</code> or <code>Service</code> must implement the <code>IBeaconConsuemr</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(IBeaconConsumer consumer) { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to SDK 18. Method invocation will be ignored"); return; } synchronized (consumers) { if (consumers.keySet().contains(consumer)) { if (IBeaconManager.debug) Log.d(TAG, "This consumer is already bound"); } else { if (IBeaconManager.debug) Log.d(TAG, "This consumer is not bound. binding: "+consumer); consumers.put(consumer, new ConsumerInfo()); Intent intent = new Intent(consumer.getApplicationContext(), IBeaconService.class); consumer.bindService(intent, iBeaconServiceConnection, Context.BIND_AUTO_CREATE); if (IBeaconManager.debug) Log.d(TAG, "consumer count is now:"+consumers.size()); if (serviceMessenger != null) { // If the serviceMessenger is not null, that means we are able to make calls to the service setBackgroundMode(consumer, false); // if we just bound, we assume we are not in the background. } } } } /** * Unbinds an Android <code>Activity</code> or <code>Service</code> to the <code>IBeaconService</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(IBeaconConsumer consumer) { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to SDK 18. Method invocation will be ignored"); return; } synchronized(consumers) { if (consumers.keySet().contains(consumer)) { Log.d(TAG, "Unbinding"); consumer.unbindService(iBeaconServiceConnection); consumers.remove(consumer); } else { if (IBeaconManager.debug) Log.d(TAG, "This consumer is not bound to: "+consumer); if (IBeaconManager.debug) Log.d(TAG, "Bound consumers: "); for (int i = 0; i < consumers.size(); i++) { Log.i(TAG, " "+consumers.get(i)); } } } } /** * Tells you if the passed iBeacon consumer is bound to the service * @param consumer * @return */ public boolean isBound(IBeaconConsumer consumer) { synchronized(consumers) { return consumers.keySet().contains(consumer) && (serviceMessenger != null); } } /** * This method notifies the iBeacon service that the IBeaconConsumer is either moving to background mode or foreground mode * When in background mode, BluetoothLE scans to look for iBeacons 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. * Note that when multiple IBeaconConsumers exist, all must be in background mode for the the background scan periods to be used * When ranging in the background, the time between updates will be much less fequent than in the foreground. Updates will come * every time interval equal to the sum total of the BackgroundScanPeriod and the BackgroundBetweenScanPeriod * All IBeaconConsumers are by default treated as being in foreground mode unless this method is explicitly called indicating * otherwise. * * @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) * @param consumer * @param backgroundMode true indicates the iBeaconConsumer is in the background * returns true if background mode is successfully set */ public boolean setBackgroundMode(IBeaconConsumer consumer, boolean backgroundMode) { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to SDK 18. Method invocation will be ignored"); return false; } synchronized(consumers) { Log.i(TAG, "setBackgroundMode for consumer"+consumer+" to "+backgroundMode); if (consumers.keySet().contains(consumer)) { try { ConsumerInfo consumerInfo = consumers.get(consumer); consumerInfo.isInBackground = backgroundMode; updateScanPeriods(); return true; } catch (RemoteException e) { Log.e(TAG, "Failed to set background mode", e); return false; } } else { if (IBeaconManager.debug) Log.d(TAG, "This consumer is not bound to: "+consumer); return false; } } } /** * Specifies a class that should be called each time the <code>IBeaconService</code> gets ranging * data, which is nominally once per second when iBeacons are detected. * * 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. * * @see RangeNotifier * @param notifier */ public void setRangeNotifier(RangeNotifier notifier) { rangeNotifier = notifier; } /** * Specifies a class that should be called each time the <code>IBeaconService</code> gets sees * or stops seeing a Region of iBeacons. * * IMPORTANT: Only one MonitorNotifier 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. * * @see MonitorNotifier * @see #startMonitoringBeaconsInRegion(Region region) * @see Region * @param notifier */ public void setMonitorNotifier(MonitorNotifier notifier) { monitorNotifier = notifier; } /** * Tells the <code>IBeaconService</code> to start looking for iBeacons that match the passed * <code>Region</code> object, and providing updates on the estimated distance very seconds while * iBeacons in the Region are visible. Note that the Region's unique identifier must be retained to * later call the stopRangingBeaconsInRegion method. * * @see IBeaconManager#setRangeNotifier(RangeNotifier) * @see IBeaconManager#stopRangingBeaconsInRegion(Region region) * @see RangeNotifier * @see Region * @param region */ @TargetApi(18) public void startRangingBeaconsInRegion(Region region) throws RemoteException { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to SDK 18. Method invocation will be ignored"); return; } if (serviceMessenger == null) { throw new RemoteException("The IBeaconManager is not bound to the service. Call iBeaconManager.bind(IBeaconConsumer consumer) and wait for a callback to onIBeaconServiceConnect()"); } Message msg = Message.obtain(null, IBeaconService.MSG_START_RANGING, 0, 0); StartRMData obj = new StartRMData(new RegionData(region), callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod() ); msg.obj = obj; serviceMessenger.send(msg); synchronized (rangedRegions) { rangedRegions.add((Region) region.clone()); } } /** * Tells the <code>IBeaconService</code> to stop looking for iBeacons that match the passed * <code>Region</code> object and providing distance information for them. * * @see #setMonitorNotifier(MonitorNotifier notifier) * @see #startMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region * @param region */ @TargetApi(18) public void stopRangingBeaconsInRegion(Region region) throws RemoteException { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to SDK 18. Method invocation will be ignored"); return; } if (serviceMessenger == null) { throw new RemoteException("The IBeaconManager is not bound to the service. Call iBeaconManager.bind(IBeaconConsumer consumer) and wait for a callback to onIBeaconServiceConnect()"); } Message msg = Message.obtain(null, IBeaconService.MSG_STOP_RANGING, 0, 0); StartRMData obj = new StartRMData(new RegionData(region), callbackPackageName(),this.getScanPeriod(), this.getBetweenScanPeriod() ); msg.obj = obj; serviceMessenger.send(msg); synchronized (rangedRegions) { Region regionToRemove = null; for (Region rangedRegion : rangedRegions) { if (region.getUniqueId().equals(rangedRegion.getUniqueId())) { regionToRemove = rangedRegion; } } rangedRegions.remove(regionToRemove); } } /** * Tells the <code>IBeaconService</code> to start looking for iBeacons that match the passed * <code>Region</code> object. Note that the Region's unique identifier must be retained to * later call the stopMonitoringBeaconsInRegion method. * * @see IBeaconManager#setMonitorNotifier(MonitorNotifier) * @see IBeaconManager#stopMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region * @param region */ @TargetApi(18) public void startMonitoringBeaconsInRegion(Region region) throws RemoteException { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to API 18. Method invocation will be ignored"); return; } if (serviceMessenger == null) { throw new RemoteException("The IBeaconManager is not bound to the service. Call iBeaconManager.bind(IBeaconConsumer consumer) and wait for a callback to onIBeaconServiceConnect()"); } Message msg = Message.obtain(null, IBeaconService.MSG_START_MONITORING, 0, 0); StartRMData obj = new StartRMData(new RegionData(region), callbackPackageName(),this.getScanPeriod(), this.getBetweenScanPeriod() ); msg.obj = obj; serviceMessenger.send(msg); synchronized (monitoredRegions) { monitoredRegions.add((Region) region.clone()); } } /** * Tells the <code>IBeaconService</code> to stop looking for iBeacons that match the passed * <code>Region</code> object. Note that the Region's unique identifier is used to match it to * and existing monitored Region. * * @see IBeaconManager#setMonitorNotifier(MonitorNotifier) * @see IBeaconManager#startMonitoringBeaconsInRegion(Region region) * @see MonitorNotifier * @see Region * @param region */ @TargetApi(18) public void stopMonitoringBeaconsInRegion(Region region) throws RemoteException { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to API 18. Method invocation will be ignored"); return; } if (serviceMessenger == null) { throw new RemoteException("The IBeaconManager is not bound to the service. Call iBeaconManager.bind(IBeaconConsumer consumer) and wait for a callback to onIBeaconServiceConnect()"); } Message msg = Message.obtain(null, IBeaconService.MSG_STOP_MONITORING, 0, 0); StartRMData obj = new StartRMData(new RegionData(region), callbackPackageName(),this.getScanPeriod(), this.getBetweenScanPeriod() ); msg.obj = obj; serviceMessenger.send(msg); synchronized (monitoredRegions) { Region regionToRemove = null; for (Region monitoredRegion : monitoredRegions) { if (region.getUniqueId().equals(monitoredRegion.getUniqueId())) { regionToRemove = monitoredRegion; } } monitoredRegions.remove(regionToRemove); } } /** 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 IBeaconManager is not bound to the service. */ @TargetApi(18) public void updateScanPeriods() throws RemoteException { if (android.os.Build.VERSION.SDK_INT < 18) { Log.w(TAG, "Not supported prior to API 18. Method invocation will be ignored"); return; } if (serviceMessenger == null) { throw new RemoteException("The IBeaconManager is not bound to the service. Call iBeaconManager.bind(IBeaconConsumer consumer) and wait for a callback to onIBeaconServiceConnect()"); } Message msg = Message.obtain(null, IBeaconService.MSG_SET_SCAN_PERIODS, 0, 0); Log.d(TAG, "updating scan period to "+this.getScanPeriod()+", "+this.getBetweenScanPeriod() ); StartRMData obj = new StartRMData(this.getScanPeriod(), this.getBetweenScanPeriod()); msg.obj = obj; serviceMessenger.send(msg); } /** * @deprecated Use updateScanPeriods() * @throws RemoteException */ public void setScanPeriods() throws RemoteException { updateScanPeriods(); } private String callbackPackageName() { String packageName = context.getPackageName(); if (IBeaconManager.debug) Log.d(TAG, "callback packageName: "+packageName); return packageName; } private ServiceConnection iBeaconServiceConnection = new ServiceConnection() { // Called when the connection with the service is established public void onServiceConnected(ComponentName className, IBinder service) { if (IBeaconManager.debug) Log.d(TAG, "we have a connection to the service now"); serviceMessenger = new Messenger(service); synchronized(consumers) { Iterator<IBeaconConsumer> consumerIterator = consumers.keySet().iterator(); while (consumerIterator.hasNext()) { IBeaconConsumer consumer = consumerIterator.next(); Boolean alreadyConnected = consumers.get(consumer).isConnected; if (!alreadyConnected) { consumer.onIBeaconServiceConnect(); ConsumerInfo consumerInfo = consumers.get(consumer); consumerInfo.isConnected = true; consumers.put(consumer,consumerInfo); } } } } // Called when the connection with the service disconnects unexpectedly public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "onServiceDisconnected"); } }; /** * @see #monitorNotifier * @return monitorNotifier */ public MonitorNotifier getMonitoringNotifier() { return this.monitorNotifier; } /** * @see #rangeNotifier * @return rangeNotifier */ public RangeNotifier getRangingNotifier() { return this.rangeNotifier; } /** * @return the list of regions currently being monitored */ public Collection<Region> getMonitoredRegions() { ArrayList<Region> clonedMontoredRegions = new ArrayList<Region>(); synchronized(this.monitoredRegions) { for (Region montioredRegion : this.monitoredRegions) { clonedMontoredRegions.add((Region) montioredRegion.clone()); } } return clonedMontoredRegions; } /** * @return the list of regions currently being ranged */ public Collection<Region> getRangedRegions() { ArrayList<Region> clonedRangedRegions = new ArrayList<Region>(); synchronized(this.rangedRegions) { for (Region rangedRegion : this.rangedRegions) { clonedRangedRegions.add((Region) rangedRegion.clone()); } } return clonedRangedRegions; } protected static BeaconSimulator beaconSimulator; public static void setBeaconSimulator(BeaconSimulator beaconSimulator) { IBeaconManager.beaconSimulator = beaconSimulator; } public static BeaconSimulator getBeaconSimulator() { return IBeaconManager.beaconSimulator; } protected void setDataRequestNotifier(RangeNotifier notifier) { this.dataRequestNotifier = notifier; } protected RangeNotifier getDataRequestNotifier() { return this.dataRequestNotifier; } private class ConsumerInfo { public boolean isConnected = false; public boolean isInBackground = false; } private boolean isInBackground() { boolean background = true; synchronized(consumers) { for (IBeaconConsumer consumer : consumers.keySet()) { if (!consumers.get(consumer).isInBackground) { background = false; } if (debug) Log.d(TAG, "Consumer "+consumer+" isInBackground="+consumers.get(consumer).isInBackground); } } if (debug) Log.d(TAG, "Overall background mode is therefore "+background); return background; } private long getScanPeriod() { if (isInBackground()) { return backgroundScanPeriod; } else { return foregroundScanPeriod; } } private long getBetweenScanPeriod() { if (isInBackground()) { return backgroundBetweenScanPeriod; } else { return foregroundBetweenScanPeriod; } } }