/**
* Radius Networks, Inc.
* http://www.radiusnetworks.com
*
* @author David G. Young
* <p/>
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.service;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.BuildConfig;
import org.altbeacon.beacon.Region;
import org.altbeacon.beacon.distance.DistanceCalculator;
import org.altbeacon.beacon.distance.ModelSpecificDistanceCalculator;
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.service.scanner.CycledLeScanCallback;
import org.altbeacon.beacon.service.scanner.CycledLeScanner;
import org.altbeacon.beacon.service.scanner.DistinctPacketDetector;
import org.altbeacon.beacon.service.scanner.NonBeaconLeScanCallback;
import org.altbeacon.beacon.startup.StartupBroadcastReceiver;
import org.altbeacon.beacon.utils.ProcessUtils;
import org.altbeacon.bluetooth.BluetoothCrashResolver;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.app.PendingIntent.getBroadcast;
/**
* @author dyoung
*/
public class BeaconService extends Service {
public static final String TAG = "BeaconService";
private final Map<Region, RangeState> rangedRegionState = new HashMap<Region, RangeState>();
private MonitoringStatus monitoringStatus;
int trackedBeaconsPacketCount;
private final Handler handler = new Handler();
private BluetoothCrashResolver bluetoothCrashResolver;
private DistanceCalculator defaultDistanceCalculator = null;
private BeaconManager beaconManager;
private Set<BeaconParser> beaconParsers = new HashSet<BeaconParser>();
private CycledLeScanner mCycledScanner;
private boolean mBackgroundFlag = false;
private ExtraDataBeaconTracker mExtraDataBeaconTracker;
private ExecutorService mExecutor;
private final DistinctPacketDetector mDistinctPacketDetector = new DistinctPacketDetector();
/*
* The scan period is how long we wait between restarting the BLE advertisement scans
* Each time we restart we only see the unique advertisements once (e.g. unique beacons)
* So if we want updates, we have to restart. For updates at 1Hz, ideally we
* would restart scanning that often to get the same update rate. The trouble is that when you
* restart scanning, it is not instantaneous, and you lose any beacon packets that were in the
* air during the restart. So the more frequently you restart, the more packets you lose. The
* frequency is therefore a tradeoff. Testing with 14 beacons, transmitting once per second,
* here are the counts I got for various values of the SCAN_PERIOD:
*
* Scan period Avg beacons % missed
* 1s 6 57
* 2s 10 29
* 3s 12 14
* 5s 14 0
*
* Also, because beacons transmit once per second, the scan period should not be an even multiple
* of seconds, because then it may always miss a beacon that is synchronized with when it is stopping
* scanning.
*
*/
private List<Beacon> simulatedScanData = null;
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class BeaconBinder extends Binder {
public BeaconService getService() {
LogManager.i(TAG, "getService of BeaconBinder called");
// Return this instance of LocalService so clients can call public methods
return BeaconService.this;
}
}
/**
* Command to the service to display a message
*/
public static final int MSG_START_RANGING = 2;
public static final int MSG_STOP_RANGING = 3;
public static final int MSG_START_MONITORING = 4;
public static final int MSG_STOP_MONITORING = 5;
public static final int MSG_SET_SCAN_PERIODS = 6;
public static final int MSG_SYNC_SETTINGS = 7;
static class IncomingHandler extends Handler {
private final WeakReference<BeaconService> mService;
IncomingHandler(BeaconService service) {
mService = new WeakReference<BeaconService>(service);
}
@Override
public void handleMessage(Message msg) {
BeaconService service = mService.get();
if (service != null) {
StartRMData startRMData = StartRMData.fromBundle(msg.getData());
if (startRMData != null) {
switch (msg.what) {
case MSG_START_RANGING:
LogManager.i(TAG, "start ranging received");
service.startRangingBeaconsInRegion(startRMData.getRegionData(), new org.altbeacon.beacon.service.Callback(startRMData.getCallbackPackageName()));
service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod(), startRMData.getBackgroundFlag());
break;
case MSG_STOP_RANGING:
LogManager.i(TAG, "stop ranging received");
service.stopRangingBeaconsInRegion(startRMData.getRegionData());
service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod(), startRMData.getBackgroundFlag());
break;
case MSG_START_MONITORING:
LogManager.i(TAG, "start monitoring received");
service.startMonitoringBeaconsInRegion(startRMData.getRegionData(), new org.altbeacon.beacon.service.Callback(startRMData.getCallbackPackageName()));
service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod(), startRMData.getBackgroundFlag());
break;
case MSG_STOP_MONITORING:
LogManager.i(TAG, "stop monitoring received");
service.stopMonitoringBeaconsInRegion(startRMData.getRegionData());
service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod(), startRMData.getBackgroundFlag());
break;
case MSG_SET_SCAN_PERIODS:
LogManager.i(TAG, "set scan intervals received");
service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod(), startRMData.getBackgroundFlag());
break;
default:
super.handleMessage(msg);
}
}
else if (msg.what == MSG_SYNC_SETTINGS) {
LogManager.i(TAG, "Received settings update from other process");
SettingsData settingsData = SettingsData.fromBundle(msg.getData());
if (settingsData != null) {
settingsData.apply(service);
}
else {
LogManager.w(TAG, "Settings data missing");
}
}
else {
LogManager.i(TAG, "Received unknown message from other process : "+msg.what);
}
}
}
}
/**
* Target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new IncomingHandler(this));
@Override
public void onCreate() {
bluetoothCrashResolver = new BluetoothCrashResolver(this);
bluetoothCrashResolver.start();
// Create a private executor so we don't compete with threads used by AsyncTask
// This uses fewer threads than the default executor so it won't hog CPU
mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);
mCycledScanner = CycledLeScanner.createScanner(this, BeaconManager.DEFAULT_FOREGROUND_SCAN_PERIOD,
BeaconManager.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD, mBackgroundFlag, mCycledLeScanCallback, bluetoothCrashResolver);
beaconManager = BeaconManager.getInstanceForApplication(getApplicationContext());
beaconManager.setScannerInSameProcess(true);
if (beaconManager.isMainProcess()) {
LogManager.i(TAG, "beaconService version %s is starting up on the main process", BuildConfig.VERSION_NAME);
}
else {
LogManager.i(TAG, "beaconService version %s is starting up on a separate process", BuildConfig.VERSION_NAME);
ProcessUtils processUtils = new ProcessUtils(this);
LogManager.i(TAG, "beaconService PID is "+processUtils.getPid()+" with process name "+processUtils.getProcessName());
}
reloadParsers();
defaultDistanceCalculator = new ModelSpecificDistanceCalculator(this, BeaconManager.getDistanceModelUpdateUrl());
Beacon.setDistanceCalculator(defaultDistanceCalculator);
monitoringStatus = MonitoringStatus.getInstanceForApplication(getApplicationContext());
// Look for simulated scan data
try {
Class klass = Class.forName("org.altbeacon.beacon.SimulatedScanData");
java.lang.reflect.Field f = klass.getField("beacons");
this.simulatedScanData = (List<Beacon>) f.get(null);
} catch (ClassNotFoundException e) {
LogManager.d(TAG, "No org.altbeacon.beacon.SimulatedScanData class exists.");
} catch (Exception e) {
LogManager.e(e, TAG, "Cannot get simulated Scan data. Make sure your org.altbeacon.beacon.SimulatedScanData class defines a field with the signature 'public static List<Beacon> beacons'");
}
}
protected void reloadParsers() {
HashSet<BeaconParser> newBeaconParsers = new HashSet<BeaconParser>();
//flatMap all beacon parsers
boolean matchBeaconsByServiceUUID = true;
if (beaconManager.getBeaconParsers() != null) {
newBeaconParsers.addAll(beaconManager.getBeaconParsers());
for (BeaconParser beaconParser : beaconManager.getBeaconParsers()) {
if (beaconParser.getExtraDataParsers().size() > 0) {
matchBeaconsByServiceUUID = false;
newBeaconParsers.addAll(beaconParser.getExtraDataParsers());
}
}
}
beaconParsers = newBeaconParsers;
//initialize the extra data beacon tracker
mExtraDataBeaconTracker = new ExtraDataBeaconTracker(matchBeaconsByServiceUUID);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogManager.i(TAG,
intent == null ?
"starting with null intent"
:
"starting with intent " + intent.toString()
);
return super.onStartCommand(intent, flags, startId);
}
/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
@Override
public IBinder onBind(Intent intent) {
LogManager.i(TAG, "binding");
return mMessenger.getBinder();
}
@Override
public boolean onUnbind(Intent intent) {
LogManager.i(TAG, "unbinding");
return false;
}
@Override
public void onDestroy() {
LogManager.e(TAG, "onDestroy()");
if (android.os.Build.VERSION.SDK_INT < 18) {
LogManager.w(TAG, "Not supported prior to API 18.");
return;
}
bluetoothCrashResolver.stop();
LogManager.i(TAG, "onDestroy called. stopping scanning");
handler.removeCallbacksAndMessages(null);
mCycledScanner.stop();
mCycledScanner.destroy();
monitoringStatus.stopStatusPreservation();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
LogManager.d(TAG, "task removed");
if (Build.VERSION.RELEASE.contains("4.4.1") ||
Build.VERSION.RELEASE.contains("4.4.2") ||
Build.VERSION.RELEASE.contains("4.4.3")) {
AlarmManager alarmManager = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, getRestartIntent());
LogManager.d(TAG, "Setting a wakeup alarm to go off due to Android 4.4.2 service restarting bug.");
}
}
private PendingIntent getRestartIntent() {
Intent restartIntent = new Intent(getApplicationContext(), StartupBroadcastReceiver.class);
return getBroadcast(getApplicationContext(), 1, restartIntent, FLAG_ONE_SHOT);
}
/**
* methods for clients
*/
public void startRangingBeaconsInRegion(Region region, Callback callback) {
synchronized (rangedRegionState) {
if (rangedRegionState.containsKey(region)) {
LogManager.i(TAG, "Already ranging that region -- will replace existing region.");
rangedRegionState.remove(region); // need to remove it, otherwise the old object will be retained because they are .equal //FIXME That is not true
}
rangedRegionState.put(region, new RangeState(callback));
LogManager.d(TAG, "Currently ranging %s regions.", rangedRegionState.size());
}
mCycledScanner.start();
}
public void stopRangingBeaconsInRegion(Region region) {
int rangedRegionCount;
synchronized (rangedRegionState) {
rangedRegionState.remove(region);
rangedRegionCount = rangedRegionState.size();
LogManager.d(TAG, "Currently ranging %s regions.", rangedRegionState.size());
}
if (rangedRegionCount == 0 && monitoringStatus.regionsCount() == 0) {
mCycledScanner.stop();
}
}
public void startMonitoringBeaconsInRegion(Region region, Callback callback) {
LogManager.d(TAG, "startMonitoring called");
monitoringStatus.addRegion(region, callback);
LogManager.d(TAG, "Currently monitoring %s regions.", monitoringStatus.regionsCount());
mCycledScanner.start();
}
public void stopMonitoringBeaconsInRegion(Region region) {
LogManager.d(TAG, "stopMonitoring called");
monitoringStatus.removeRegion(region);
LogManager.d(TAG, "Currently monitoring %s regions.", monitoringStatus.regionsCount());
if (monitoringStatus.regionsCount() == 0 && rangedRegionState.size() == 0) {
mCycledScanner.stop();
}
}
public void setScanPeriods(long scanPeriod, long betweenScanPeriod, boolean backgroundFlag) {
mCycledScanner.setScanPeriods(scanPeriod, betweenScanPeriod, backgroundFlag);
}
protected final CycledLeScanCallback mCycledLeScanCallback = new CycledLeScanCallback() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
NonBeaconLeScanCallback nonBeaconLeScanCallback = beaconManager.getNonBeaconLeScanCallback();
try {
new ScanProcessor(nonBeaconLeScanCallback).executeOnExecutor(mExecutor,
new ScanData(device, rssi, scanRecord));
} catch (RejectedExecutionException e) {
LogManager.w(TAG, "Ignoring scan result because we cannot keep up.");
}
}
@Override
public void onCycleEnd() {
mDistinctPacketDetector.clearDetections();
monitoringStatus.updateNewlyOutside();
processRangeData();
// If we want to use simulated scanning data, do it here. This is used for testing in an emulator
if (simulatedScanData != null) {
// if simulatedScanData is provided, it will be seen every scan cycle. *in addition* to anything actually seen in the air
// it will not be used if we are not in debug mode
LogManager.w(TAG, "Simulated scan data is deprecated and will be removed in a future release. Please use the new BeaconSimulator interface instead.");
if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) {
for (Beacon beacon : simulatedScanData) {
processBeaconFromScan(beacon);
}
} else {
LogManager.w(TAG, "Simulated scan data provided, but ignored because we are not running in debug mode. Please remove simulated scan data for production.");
}
}
if (BeaconManager.getBeaconSimulator() != null) {
// if simulatedScanData is provided, it will be seen every scan cycle. *in addition* to anything actually seen in the air
// it will not be used if we are not in debug mode
if (BeaconManager.getBeaconSimulator().getBeacons() != null) {
if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) {
for (Beacon beacon : BeaconManager.getBeaconSimulator().getBeacons()) {
processBeaconFromScan(beacon);
}
} else {
LogManager.w(TAG, "Beacon simulations provided, but ignored because we are not running in debug mode. Please remove beacon simulations for production.");
}
} else {
LogManager.w(TAG, "getBeacons is returning null. No simulated beacons to report.");
}
}
}
};
private void processRangeData() {
synchronized (rangedRegionState) {
for (Region region : rangedRegionState.keySet()) {
RangeState rangeState = rangedRegionState.get(region);
LogManager.d(TAG, "Calling ranging callback");
rangeState.getCallback().call(BeaconService.this, "rangingData", new RangingData(rangeState.finalizeBeacons(), region).toBundle());
}
}
}
private void processBeaconFromScan(Beacon beacon) {
if (Stats.getInstance().isEnabled()) {
Stats.getInstance().log(beacon);
}
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG,
"beacon detected : %s", beacon.toString());
}
beacon = mExtraDataBeaconTracker.track(beacon);
// If this is a Gatt beacon that should be ignored, it will be set to null as a result of
// the above
if (beacon == null) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG,
"not processing detections for GATT extra data beacon");
}
} else {
monitoringStatus.updateNewlyInsideInRegionsContaining(beacon);
List<Region> matchedRegions = null;
Iterator<Region> matchedRegionIterator;
LogManager.d(TAG, "looking for ranging region matches for this beacon");
synchronized (rangedRegionState) {
matchedRegions = matchingRegions(beacon, rangedRegionState.keySet());
matchedRegionIterator = matchedRegions.iterator();
while (matchedRegionIterator.hasNext()) {
Region region = matchedRegionIterator.next();
LogManager.d(TAG, "matches ranging region: %s", region);
RangeState rangeState = rangedRegionState.get(region);
if (rangeState != null) {
rangeState.addBeacon(beacon);
}
}
}
}
}
private class ScanData {
public ScanData(BluetoothDevice device, int rssi, byte[] scanRecord) {
this.device = device;
this.rssi = rssi;
this.scanRecord = scanRecord;
}
int rssi;
BluetoothDevice device;
byte[] scanRecord;
}
private class ScanProcessor extends AsyncTask<ScanData, Void, Void> {
final DetectionTracker mDetectionTracker = DetectionTracker.getInstance();
private final NonBeaconLeScanCallback mNonBeaconLeScanCallback;
public ScanProcessor(NonBeaconLeScanCallback nonBeaconLeScanCallback) {
mNonBeaconLeScanCallback = nonBeaconLeScanCallback;
}
@Override
protected Void doInBackground(ScanData... params) {
ScanData scanData = params[0];
Beacon beacon = null;
for (BeaconParser parser : BeaconService.this.beaconParsers) {
beacon = parser.fromScanData(scanData.scanRecord,
scanData.rssi, scanData.device);
if (beacon != null) {
break;
}
}
if (beacon != null) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "Beacon packet detected for: "+beacon+" with rssi "+beacon.getRssi());
}
mDetectionTracker.recordDetection();
if (!mCycledScanner.getDistinctPacketsDetectedPerScan()) {
if (!mDistinctPacketDetector.isPacketDistinct(scanData.device.getAddress(),
scanData.scanRecord)) {
LogManager.i(TAG, "Non-distinct packets detected in a single scan. Restarting scans unecessary.");
mCycledScanner.setDistinctPacketsDetectedPerScan(true);
}
}
trackedBeaconsPacketCount++;
processBeaconFromScan(beacon);
} else {
if (mNonBeaconLeScanCallback != null) {
mNonBeaconLeScanCallback.onNonBeaconLeScan(scanData.device, scanData.rssi, scanData.scanRecord);
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
}
@Override
protected void onPreExecute() {
}
@Override
protected void onProgressUpdate(Void... values) {
}
}
private List<Region> matchingRegions(Beacon beacon, Collection<Region> regions) {
List<Region> matched = new ArrayList<Region>();
for (Region region : regions) {
if (region.matchesBeacon(beacon)) {
matched.add(region);
} else {
LogManager.d(TAG, "This region (%s) does not match beacon: %s", region, beacon);
}
}
return matched;
}
}