/**
* 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.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
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.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import com.radiusnetworks.ibeacon.client.RangingTracker;
import com.radiusnetworks.ibeacon.service.IBeaconData;
import com.radiusnetworks.ibeacon.service.IBeaconService;
import com.radiusnetworks.ibeacon.service.RangingData;
import com.radiusnetworks.ibeacon.service.RegionData;
import com.radiusnetworks.ibeacon.service.StartRMData;
/**
* 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
*
*/
public class IBeaconManager {
private static final String TAG = "IBeaconManager";
private Context context;
private static IBeaconManager client = null;
private Map<IBeaconConsumer, Boolean> consumers = new HashMap<IBeaconConsumer, Boolean>();
private Messenger serviceMessenger = null;
protected RangeNotifier rangeNotifier = null;
protected MonitorNotifier monitorNotifier = null;
protected RangingTracker rangingTracker = new RangingTracker();
/**
* 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 (!isInstantiated()) {
Log.d(TAG, "IBeaconManager instance craetion");
client = new IBeaconManager(context);
}
return client;
}
private 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 a
* RuntimeException if Bluetooth LE is not supported. (Note: The Android emulator will do this)
*
* @return false if it is supported and not enabled
*/
public boolean checkAvailability() {
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 (consumers.keySet().contains(consumer)) {
Log.i(TAG, "This consumer is already bound");
} else {
Log.i(TAG, "This consumer is not bound. binding: " + consumer);
consumers.put(consumer, false);
Intent intent = new Intent(consumer.getApplicationContext(), IBeaconService.class);
consumer.bindService(intent, iBeaconServiceConnection, Context.BIND_AUTO_CREATE);
Log.i(TAG, "consumer count is now:" + consumers.size());
}
}
/**
* 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 (consumers.keySet().contains(consumer)) {
Log.i(TAG, "Unbinding");
consumer.unbindService(iBeaconServiceConnection);
consumers.remove(consumer);
} else {
Log.i(TAG, "This consumer is not bound to: " + consumer);
Log.i(TAG, "Bound consumers: ");
for (int i = 0; i < consumers.size(); i++) {
Log.i(TAG, " " + consumers.get(i));
}
}
}
/**
* 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.
*
* @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.
*
* @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
*/
public void startRangingBeaconsInRegion(Region region) throws RemoteException {
Message msg = Message.obtain(null, IBeaconService.MSG_START_RANGING, 0, 0);
StartRMData obj = new StartRMData(new RegionData(region), rangingCallbackAction());
msg.obj = obj;
msg.replyTo = rangingCallback;
serviceMessenger.send(msg);
}
/**
* 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
*/
public void stopRangingBeaconsInRegion(Region region) throws RemoteException {
Message msg = Message.obtain(null, IBeaconService.MSG_STOP_RANGING, 0, 0);
StartRMData obj = new StartRMData(new RegionData(region), rangingCallbackAction());
msg.obj = obj;
serviceMessenger.send(msg);
}
/**
* 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
*/
public void startMonitoringBeaconsInRegion(Region region) throws RemoteException {
Message msg = Message.obtain(null, IBeaconService.MSG_START_MONITORING, 0, 0);
StartRMData obj = new StartRMData(new RegionData(region), monitoringCallbackAction());
msg.obj = obj;
msg.replyTo = null; // TODO: remove this when we are converted to Intents
serviceMessenger.send(msg);
}
/**
* 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
*/
public void stopMonitoringBeaconsInRegion(Region region) throws RemoteException {
Message msg = Message.obtain(null, IBeaconService.MSG_STOP_MONITORING, 0, 0);
StartRMData obj = new StartRMData(new RegionData(region), rangingCallbackAction());
msg.obj = obj;
serviceMessenger.send(msg);
}
private String rangingCallbackAction() {
String action = context.getPackageName() + ".DID_RANGING";
Log.d(TAG, "ranging callback action: " + action);
return action;
}
private String monitoringCallbackAction() {
String action = context.getPackageName() + ".DID_MONITORING";
Log.d(TAG, "monitoring callback action: " + action);
return action;
}
private ServiceConnection iBeaconServiceConnection = new ServiceConnection() {
// Called when the connection with the service is established
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "we have a connection to the service now");
serviceMessenger = new Messenger(service);
Iterator<IBeaconConsumer> consumerIterator = consumers.keySet().iterator();
while (consumerIterator.hasNext()) {
IBeaconConsumer consumer = consumerIterator.next();
Boolean alreadyConnected = consumers.get(consumer);
if (!alreadyConnected) {
consumer.onIBeaconServiceConnect();
consumers.put(consumer, true);
}
}
}
// Called when the connection with the service disconnects unexpectedly
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "onServiceDisconnected");
}
};
static class IncomingHandler extends Handler {
private final WeakReference<IBeaconManager> iBeaconManager;
IncomingHandler(IBeaconManager manager) {
iBeaconManager = new WeakReference<IBeaconManager>(manager);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
default:
super.handleMessage(msg);
RangingData data = (RangingData) msg.obj;
Log.d(TAG, "Got a ranging callback with data: " + data);
Log.d(TAG, "Got a ranging callback with " + data.getIBeacons().size() + " iBeacons");
if (data.getIBeacons() != null) {
Iterator<IBeaconData> iterator = data.getIBeacons().iterator();
while (iterator.hasNext()) {
IBeaconData iBeaconData = iterator.next();
if (iBeaconData == null) {
Log.d(TAG, "null ibeacon found");
} else {
iBeaconManager.get().rangingTracker.addIBeacon(iBeaconData);
}
}
IBeaconManager manager = iBeaconManager.get();
if (manager.rangeNotifier != null) {
Log.d(TAG, "Calling ranging notifier on :" + manager.rangeNotifier);
manager.rangeNotifier.didRangeBeaconsInRegion(
iBeaconManager.get().rangingTracker.getIBeacons(), data.getRegion());
}
}
}
}
};
final Messenger rangingCallback = new Messenger(new IncomingHandler(this));
/**
* @see #monitorNotifier
* @return monitorNotifier
*/
public MonitorNotifier getMonitoringNotifier() {
return this.monitorNotifier;
}
/**
* @see #rangeNotifier
* @return rangeNotifier
*/
public RangeNotifier getRangingNotifier() {
return this.rangeNotifier;
}
/**
* Determines if the singleton has been constructed already. Useful for not overriding settings set declaratively in
* XML
*
* @return true, if the class has been constructed
*/
public static boolean isInstantiated() {
return (client != null);
}
}