From 392007a12adfb0830385b0955366a6a6f5bd2047 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Fri, 2 Jan 2015 09:35:25 -0500 Subject: [PATCH 1/6] restart scanning on Android 5 so we repeatedly see connectable beacons --- .../beacon/service/CycledLeScanner.java | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java index 4a48300d4..05f71b84c 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java @@ -156,7 +156,7 @@ public void stop() { private boolean deferScanIfNeeded() { if (mUseAndroidLScanner) { - // never stop scanning on Android L + // never defer scanning on Android L - OS handles power savings return false; } long millisecondsUntilStart = mNextScanCycleStartTime - (new Date().getTime()); @@ -307,21 +307,8 @@ private void finishScanCycle() { try { Log.d(TAG, "stopping bluetooth le scan"); if (mUseAndroidLScanner) { - if (mBetweenScanPeriod == 0) { - // Prior to Android L we had to stop scanning at the end of each - // cycle, even the betweenScanPeriod was set to zero, and then - // immediately restart. This is because on the old APIS, connectable - // advertisements only were passed along to the callback the first - // time seen in a scan period. This is no longer true with the new - // Android L apis. All advertisements are passed along even for - // connectable advertisements. So there is no need to stop scanning - // if we are just going to start back up again. - BeaconManager.logDebug(TAG, "Aborting stop scan because this is Android L"); - } - else { - mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); - mScanningPaused = true; - } + mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); + mScanningPaused = true; } else { // Yes, this is deprecated as of API21. But we still use it for devices From 9c997dc225f80328431f06904269e221a2c47a7b Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Fri, 9 Jan 2015 10:19:45 -0500 Subject: [PATCH 2/6] prepare to use ScanFilters on Android 5 --- .../org/altbeacon/beacon/AltBeaconParser.java | 1 + .../org/altbeacon/beacon/BeaconManager.java | 11 ++++ .../org/altbeacon/beacon/BeaconParser.java | 41 +++++++++++++++ .../beacon/service/CycledLeScanner.java | 52 ++++++++++++++++++- 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/altbeacon/beacon/AltBeaconParser.java b/src/main/java/org/altbeacon/beacon/AltBeaconParser.java index 82cf7dacc..95e58f037 100644 --- a/src/main/java/org/altbeacon/beacon/AltBeaconParser.java +++ b/src/main/java/org/altbeacon/beacon/AltBeaconParser.java @@ -41,6 +41,7 @@ public class AltBeaconParser extends BeaconParser { */ public AltBeaconParser() { super(); + mHardwareAssistManufacturers = new int[]{0x0118}; // Radius networks this.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"); } /** diff --git a/src/main/java/org/altbeacon/beacon/BeaconManager.java b/src/main/java/org/altbeacon/beacon/BeaconManager.java index 987bf47f4..e639472cf 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconManager.java +++ b/src/main/java/org/altbeacon/beacon/BeaconManager.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -193,7 +194,17 @@ public static BeaconManager getInstanceForApplication(Context context) { return client; } + /** + * Gets a list of the active beaconParsers. This list may only be modified before any consumers + * are bound to the beacon service + * + * @return list of active BeaconParsers + */ + public List getBeaconParsers() { + if (isAnyConsumerBound()) { + return Collections.unmodifiableList(beaconParsers); + } return beaconParsers; } diff --git a/src/main/java/org/altbeacon/beacon/BeaconParser.java b/src/main/java/org/altbeacon/beacon/BeaconParser.java index 1559e6ef5..7d86206b3 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconParser.java +++ b/src/main/java/org/altbeacon/beacon/BeaconParser.java @@ -49,6 +49,7 @@ public class BeaconParser { protected Integer mMatchingBeaconTypeCodeEndOffset; protected Integer mPowerStartOffset; protected Integer mPowerEndOffset; + protected int[] mHardwareAssistManufacturers = new int[] { 0x004c }; /** * Makes a new BeaconParser. Should normally be immediately followed by a call to #setLayout @@ -201,6 +202,30 @@ public BeaconParser setBeaconLayout(String beaconLayout) { return this; } + /** + * Returns a list of bluetooth manufactuer codes which will be used for hardware-assisted + * accelerated looking for this beacon type + * + * The possible codes are defined onthis list: + * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers + * + * @return manufacturers + */ + public int[] getHardwareAssistManufacturers() { + return mHardwareAssistManufacturers; + } + + /** + * Sets a list of bluetooth manufactuer codes which will be used for hardware-assisted + * accelerated looking for this beacon type + * + * The possible codes are defined onthis list: + * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers + * + */ + public void setHardwareAssistManufacturerCodes(int[] manufacturers) { + mHardwareAssistManufacturers = manufacturers; + } /** * @see #mMatchingBeaconTypeCode @@ -210,6 +235,22 @@ public Long getMatchingBeaconTypeCode() { return mMatchingBeaconTypeCode; } + /** + * see #mMatchingBeaconTypeCodeStartOffset + * @return + */ + public int getMatchingBeaconTypeCodeStartOffset() { + return mMatchingBeaconTypeCodeStartOffset; + } + + /** + * see #mMatchingBeaconTypeCodeEndOffset + * @return + */ + public int getMatchingBeaconTypeCodeEndOffset() { + return mMatchingBeaconTypeCodeEndOffset; + } + /** * Construct a Beacon from a Bluetooth LE packet collected by Android's Bluetooth APIs, * including the raw bluetooth device info diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java index 205eb4260..36ef1d4d8 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java @@ -18,6 +18,7 @@ import android.util.Log; import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.BeaconParser; import org.altbeacon.beacon.startup.StartupBroadcastReceiver; import org.altbeacon.bluetooth.BluetoothCrashResolver; @@ -50,6 +51,7 @@ public class CycledLeScanner { private boolean mBackgroundFlag = false; private boolean mRestartNeeded = false; private long mWakeupAlarmTime = 0l; + private BeaconManager mBeaconManager; public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { mScanPeriod = scanPeriod; @@ -58,6 +60,7 @@ public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, mCycledLeScanCallback = cycledLeScanCallback; mBluetoothCrashResolver = crashResolver; mBackgroundFlag = backgroundFlag; + mBeaconManager = BeaconManager.getInstanceForApplication(mContext); if (android.os.Build.VERSION.SDK_INT < 21) { Log.i(TAG, "This is not Android 5.0. We are using old scanning APIs"); @@ -83,7 +86,7 @@ public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, * @param backgroundFlag */ public void setScanPeriods(long scanPeriod, long betweenScanPeriod, boolean backgroundFlag) { - BeaconManager.logDebug(TAG, "Set scan periods called with "+scanPeriod+", "+betweenScanPeriod+" Background mode must have changed."); + BeaconManager.logDebug(TAG, "Set scan periods called with " + scanPeriod + ", " + betweenScanPeriod + " Background mode must have changed."); if (mBackgroundFlag != backgroundFlag) { mRestartNeeded = true; } @@ -452,10 +455,55 @@ private void setWakeUpAlarm() { @TargetApi(3) private void cancelWakeUpAlarm() { - BeaconManager.logDebug(TAG, "cancel wakeup alarm: "+mWakeUpOperation); + BeaconManager.logDebug(TAG, "cancel wakeup alarm: " + mWakeUpOperation); if (mWakeUpOperation != null) { AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(mWakeUpOperation); } } + + /* + Android 5 scan algorithm + + Same as pre android 5, except when on the between scan period. In this period: + + * If a beacon has been seen in the past 10 seconds, don't do any scanning for the between scan period. Otherwise: + * create hardware masks for any beacon regardless of identifiers + * look for these hardware masks, and if you get one, start next scan cycle early. + * when calculating the time to the next scan cycle, male it be on the seconds modulus of the between scan period plus the scan period + + Even the simplified algorithm is an improvement over the current state, but the disadvantages vs. iOS are: + + * If a sombody else's beacon is present and yours is not yet visible when the app is in the background, you won't get the + accelerated discovery. You only get the accelerated discovery if no beacons are visible before one of your regions appears. + * Once you are in your region, detecting when you are out of your region will still take 5 minutes. + + */ + @TargetApi(21) + private List createScanFiltersFromMonitoredRegions() { + List scanFilters = new ArrayList(); + // for each beacon parser, make a filter expression that includes all its desired + // hardware manufacturers + for (BeaconParser beaconParser: mBeaconManager.getBeaconParsers()) { + for (int manufacturer : beaconParser.getHardwareAssistManufacturers()) { + long typeCode = beaconParser.getMatchingBeaconTypeCode(); + int startOffset = beaconParser.getMatchingBeaconTypeCodeStartOffset(); + int endOffset = beaconParser.getMatchingBeaconTypeCodeEndOffset(); + + byte[] filter = new byte[endOffset + 1]; + byte[] mask = new byte[endOffset + 1]; + for (int i = 0; i <= endOffset; i++) { + if (i < startOffset) { + filter[i] = 0; + mask[i] = 0; + } else { + filter[i] = (byte) (typeCode & (0xff >> (i - startOffset) * 8)); + mask[i] = (byte) 0xff; + } + } + scanFilters.add(new ScanFilter.Builder().setManufacturerData((int) manufacturer, filter, mask).build()); + } + } + return scanFilters; + } } From 61ae1a21f34afca9f392cfdc0ad8e7a18f1abcc8 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sat, 10 Jan 2015 15:08:17 -0500 Subject: [PATCH 3/6] First working version of low-power Android 5 background scanning --- build.gradle | 2 +- .../org/altbeacon/beacon/BeaconManager.java | 14 +- .../org/altbeacon/beacon/BeaconParser.java | 2 +- .../beacon/service/BeaconService.java | 2 + .../beacon/service/CycledLeScanner.java | 217 ++++++++++++++---- .../beacon/service/DetectionTracker.java | 24 ++ src/test/AndroidManifest.xml | 31 +++ .../beacon/service/CycledLeScannerTest.java | 91 ++++++++ 8 files changed, 336 insertions(+), 47 deletions(-) create mode 100644 src/main/java/org/altbeacon/beacon/service/DetectionTracker.java create mode 100644 src/test/AndroidManifest.xml create mode 100644 src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java diff --git a/build.gradle b/build.gradle index b57214413..82b032005 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ dependencies { androidTestCompile('com.squareup:fest-android:1.0.+@aar') { exclude group: 'com.android.support', module: 'support-v4' } - androidTestCompile('org.robolectric:robolectric:2.3') { + androidTestCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' diff --git a/src/main/java/org/altbeacon/beacon/BeaconManager.java b/src/main/java/org/altbeacon/beacon/BeaconManager.java index e639472cf..943dd65ae 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconManager.java +++ b/src/main/java/org/altbeacon/beacon/BeaconManager.java @@ -114,6 +114,7 @@ public class BeaconManager { */ public static boolean debug = false; private static boolean sAndroidLScanningDisabled = false; + private static boolean sManifestCheckingDisabled = false; /** * Set to true if you want to show library debugging @@ -210,7 +211,9 @@ public List getBeaconParsers() { protected BeaconManager(Context context) { mContext = context; - verifyServiceDeclaration(); + if (!sManifestCheckingDisabled) { + verifyServiceDeclaration(); + } this.beaconParsers.add(new AltBeaconParser()); } /** @@ -706,5 +709,12 @@ public static void setAndroidLScanningDisabled(boolean disabled) { sAndroidLScanningDisabled = disabled; } - + /** + * Allows disabling check of manifest for proper configuration of service. Useful for unit + * testing + * @param disabled + */ + public static void setsManifestCheckingDisabled(boolean disabled) { + sManifestCheckingDisabled = disabled; + } } diff --git a/src/main/java/org/altbeacon/beacon/BeaconParser.java b/src/main/java/org/altbeacon/beacon/BeaconParser.java index 7d86206b3..aa21e1702 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconParser.java +++ b/src/main/java/org/altbeacon/beacon/BeaconParser.java @@ -439,7 +439,7 @@ public BeaconLayoutException(String s) { } } - protected byte[] longToByteArray(long longValue, int length) { + public static byte[] longToByteArray(long longValue, int length) { byte[] array = new byte[length]; for (int i = 0; i < length; i++){ //long mask = (long) Math.pow(256.0,1.0*(length-i))-1; diff --git a/src/main/java/org/altbeacon/beacon/service/BeaconService.java b/src/main/java/org/altbeacon/beacon/service/BeaconService.java index 120f64068..988ebf694 100644 --- a/src/main/java/org/altbeacon/beacon/service/BeaconService.java +++ b/src/main/java/org/altbeacon/beacon/service/BeaconService.java @@ -421,6 +421,7 @@ public ScanData(BluetoothDevice device, int rssi, byte[] scanRecord) { } private class ScanProcessor extends AsyncTask { + DetectionTracker mDetectionTracker = DetectionTracker.getInstance(); @Override protected Void doInBackground(ScanData... params) { @@ -435,6 +436,7 @@ protected Void doInBackground(ScanData... params) { } } if (beacon != null) { + mDetectionTracker.recordDetection(); processBeaconFromScan(beacon); } return null; diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java index ee63f49f2..1295a2274 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java @@ -31,10 +31,15 @@ */ public class CycledLeScanner { private static final String TAG = "CycledLeScanner"; + private static final long BACKGROUND_L_SCAN_DETECTION_PERIOD_MILLIS = 10000l; private BluetoothAdapter mBluetoothAdapter; private long mLastScanCycleStartTime = 0l; private long mLastScanCycleEndTime = 0l; private long mNextScanCycleStartTime = 0l; + private long mBackgroundLScanStartTime = 0l; + private long mBackgroundLScanFirstDetectionTime = 0l; + private boolean mScanDeferredBefore = false; + private long mScanCycleStopTime = 0l; private boolean mScanning; private boolean mScanningPaused; @@ -158,18 +163,102 @@ public void stop() { } + + /* + Android 5 background scan algorithm (largely handled in this method) + + Same as pre-Android 5, except when on the between scan period. In this period: + + If a beacon has been seen in the past 10 seconds, don't do any scanning for the between scan period. + Otherwise: + - create hardware masks for any beacon regardless of identifiers + - look for these hardware masks, and if you get one, report the detection + when calculating the time to the next scan cycle, male it be on the seconds modulus of the between scan period plus the scan period + + This is an improvement over the current state, but the disadvantages are: + + - If a somebody else's beacon is present and yours is not yet visible when the app is in + the background, you won't get the accelerated discovery. You only get the accelerated + discovery if no beacons are visible before one of your regions appears. Getting around + this would mean setting up filters that are specific to your monitored regions, which is + a possible future improvement. + + - Once you are in your region, detecting when you go out of your region will still take + until the next scan cycle starts, which by default is five minutes. + + So the bottom line is it works like this: + - If no beacons at all are visible when your app enters the background, then a low-power + scan will continue. + - If any beacons are visible, even if they do not match a + defined region, no background scanning will occur until the next between scan period + expires (5 minutes by default) + - If a beacon is subsequently detected during this low-power scan, it will start a 10-second + background scanning period. At the end of this period, if the app is still in the background, + then no beacons will be detected until the next scan cycle starts (5 minutes max by default) + - If one of the beacons detected during this 10 second period matches a region you have defined, + a region entry callback will be sent, allowing you to bring your app to the foreground to + continue scanning if desired. + - The effective result is that if no beacons are around, and then they are discovered, you + will get a callback within a few seconds on Android L vs. up to 5 minutes on older + operating system versions. + */ + + @SuppressLint("NewApi") private boolean deferScanIfNeeded() { - if (mUseAndroidLScanner) { - // never defer scanning on Android L - OS handles power savings - return false; - } long millisecondsUntilStart = mNextScanCycleStartTime - (new Date().getTime()); if (millisecondsUntilStart > 0) { - BeaconManager.logDebug(TAG, "Waiting to start next bluetooth scan for another " + millisecondsUntilStart + " milliseconds"); + if (mUseAndroidLScanner) { + long secsSinceLastDetection = System.currentTimeMillis() - + DetectionTracker.getInstance().getLastDetectionTime(); + // If we have seen a device recently + // devices should behave like pre-Android L devices, because we don't want to drain battery + // by continuously delivering packets for beacons visible in the background + if (mScanDeferredBefore == false) { + if (secsSinceLastDetection > BACKGROUND_L_SCAN_DETECTION_PERIOD_MILLIS) { + mBackgroundLScanStartTime = System.currentTimeMillis(); + mBackgroundLScanFirstDetectionTime = 0l; + BeaconManager.logDebug(TAG, "This is Android L. Doing a filtered scan for the background."); + + // On Android L, between scan cycles do a scan with a filter looking for any beacon + // if we see one of those beacons, we need to deliver the results + ScanSettings settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build(); + + mScanner.startScan(createScanFiltersForBeaconParsers(mBeaconManager.getBeaconParsers()), settings, + (android.bluetooth.le.ScanCallback) getNewLeScanCallback()); + } + else { + BeaconManager.logDebug(TAG, "This is Android L, but we last saw a beacon only "+ + secsSinceLastDetection+" ago, so we will not keep scanning in background."); + } + } + if (mBackgroundLScanStartTime > 0l) { + // if we are in here, we have detected beacons recently in a background L scan + if (DetectionTracker.getInstance().getLastDetectionTime() > mBackgroundLScanStartTime) { + if (mBackgroundLScanFirstDetectionTime == 0l) { + mBackgroundLScanFirstDetectionTime = DetectionTracker.getInstance().getLastDetectionTime(); + } + if (System.currentTimeMillis() - mBackgroundLScanFirstDetectionTime + >= BACKGROUND_L_SCAN_DETECTION_PERIOD_MILLIS) { + // if we are in here, it has been more than 10 seconds since we detected + // a beacon in background L scanning mode. We need to stop scanning + // so we do not drain battery + BeaconManager.logDebug(TAG, "We've been detecting for a bit. Stopping Android L background scanning"); + mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); + mBackgroundLScanStartTime = 0l; + } + else { + // report the results up the chain + BeaconManager.logDebug(TAG, "Delivering Android L background scanning results"); + mCycledLeScanCallback.onCycleEnd(); + } + } + } + } + BeaconManager.logDebug(TAG, "Waiting to start full bluetooth scan for another " + millisecondsUntilStart + " milliseconds"); // Don't actually wait until the next scan time -- only wait up to 1 second. this // allows us to start scanning sooner if a consumer enters the foreground and expects // results more quickly - if (mBackgroundFlag) { + if (mScanDeferredBefore == false && mBackgroundFlag) { setWakeUpAlarm(); } mHandler.postDelayed(new Runnable() { @@ -178,8 +267,17 @@ public void run() { scanLeDevice(true); } }, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart); + mScanDeferredBefore = true; return true; } + else { + if (mBackgroundLScanStartTime > 0l) { + BeaconManager.logDebug(TAG, "Stopping Android L background scanning to start full scan"); + mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); + mBackgroundLScanStartTime = 0; + } + mScanDeferredBefore = false; + } return false; } @@ -336,7 +434,7 @@ private void finishScanCycle() { } } - mNextScanCycleStartTime = (new Date().getTime() + mBetweenScanPeriod); + mNextScanCycleStartTime = getNextScanStartTime(); if (mScanningEnabled) { scanLeDevice(true); } @@ -348,6 +446,26 @@ private void finishScanCycle() { } } + private long getNextScanStartTime() { + // Because many apps may use this library on the same device, we want to try to synchonize + // scanning as much as possible in order to save battery. Therefore, we will set the scan + // intervals to be on a predictable interval using a modulus of the system time. This may + // cause scans to start a little earlier than otherwise, but it should be acceptable. + // This way, if multiple apps on the device are using the default scan periods, then they + // will all be doing scans at the same time, thereby saving battery when none are scanning. + // This, of course, won't help at all if people set custom scan periods. But since most + // people accept the defaults, this will likely have a positive effect. + if (mBetweenScanPeriod == 0) { + return System.currentTimeMillis(); + } + long fullScanCycle = mScanPeriod + mBetweenScanPeriod; + long normalizedBetweenScanPeriod = mBetweenScanPeriod-(System.currentTimeMillis() % fullScanCycle); + BeaconManager.logDebug(TAG, "Normalizing between scan period from "+mBetweenScanPeriod+" to "+ + normalizedBetweenScanPeriod); + + return System.currentTimeMillis()+normalizedBetweenScanPeriod; + } + private Object leScanCallback; @TargetApi(18) private Object getLeScanCallback() { @@ -374,13 +492,13 @@ private Object getNewLeScanCallback() { @Override public void onScanResult(int callbackType, ScanResult scanResult) { - // callback type - // Determines how this callback was triggered. Currently could only be - // CALLBACK_TYPE_ALL_MATCHES BeaconManager.logDebug(TAG, "got record"); mCycledLeScanCallback.onLeScan(scanResult.getDevice(), scanResult.getRssi(), scanResult.getScanRecord().getBytes()); - // Don't call bluetoothcrashresolver on androidl. no need. + if (mBackgroundLScanStartTime > 0) { + mBeaconManager.logDebug(TAG, "got a filtered scan result in the background."); + } + } @Override @@ -389,6 +507,7 @@ public void onBatchScanResults (List results) { for (ScanResult scanResult : results) { mCycledLeScanCallback.onLeScan(scanResult.getDevice(), scanResult.getRssi(), scanResult.getScanRecord().getBytes()); + mBeaconManager.logDebug(TAG, "got a filtered batch scan result in the background."); } } @@ -449,46 +568,58 @@ private void cancelWakeUpAlarm() { } } - /* - Android 5 scan algorithm - - Same as pre android 5, except when on the between scan period. In this period: + class ScanFilterData { + public int manufacturer; + public byte[] filter; + public byte[] mask; + } - * If a beacon has been seen in the past 10 seconds, don't do any scanning for the between scan period. Otherwise: - * create hardware masks for any beacon regardless of identifiers - * look for these hardware masks, and if you get one, start next scan cycle early. - * when calculating the time to the next scan cycle, male it be on the seconds modulus of the between scan period plus the scan period + protected List createScanFilterDataForBeaconParser(BeaconParser beaconParser) { + ArrayList scanFilters = new ArrayList(); + for (int manufacturer : beaconParser.getHardwareAssistManufacturers()) { + long typeCode = beaconParser.getMatchingBeaconTypeCode(); + int startOffset = beaconParser.getMatchingBeaconTypeCodeStartOffset(); + int endOffset = beaconParser.getMatchingBeaconTypeCodeEndOffset(); + + // Note: the -2 here is because we want the filter and mask to start after the + // two-byte manufacturer code, and the beacon parser expression is based on offsets + // from the start of the two byte code + byte[] filter = new byte[endOffset + 1 - 2]; + byte[] mask = new byte[endOffset + 1 - 2]; + byte[] typeCodeBytes = BeaconParser.longToByteArray(typeCode, endOffset-startOffset+1); + for (int layoutIndex = 2; layoutIndex <= endOffset; layoutIndex++) { + int filterIndex = layoutIndex-2; + if (layoutIndex < startOffset) { + filter[filterIndex] = 0; + mask[filterIndex] = 0; + } else { + filter[filterIndex] = typeCodeBytes[layoutIndex-startOffset]; + mask[filterIndex] = (byte) 0xff; + } + } + ScanFilterData sfd = new ScanFilterData(); + sfd.manufacturer = manufacturer; + sfd.filter = filter; + sfd.mask = mask; + scanFilters.add(sfd); - Even the simplified algorithm is an improvement over the current state, but the disadvantages vs. iOS are: + } + return scanFilters; + } - * If a sombody else's beacon is present and yours is not yet visible when the app is in the background, you won't get the - accelerated discovery. You only get the accelerated discovery if no beacons are visible before one of your regions appears. - * Once you are in your region, detecting when you are out of your region will still take 5 minutes. - */ @TargetApi(21) - private List createScanFiltersFromMonitoredRegions() { + protected List createScanFiltersForBeaconParsers(List beaconParsers) { List scanFilters = new ArrayList(); // for each beacon parser, make a filter expression that includes all its desired // hardware manufacturers - for (BeaconParser beaconParser: mBeaconManager.getBeaconParsers()) { - for (int manufacturer : beaconParser.getHardwareAssistManufacturers()) { - long typeCode = beaconParser.getMatchingBeaconTypeCode(); - int startOffset = beaconParser.getMatchingBeaconTypeCodeStartOffset(); - int endOffset = beaconParser.getMatchingBeaconTypeCodeEndOffset(); - - byte[] filter = new byte[endOffset + 1]; - byte[] mask = new byte[endOffset + 1]; - for (int i = 0; i <= endOffset; i++) { - if (i < startOffset) { - filter[i] = 0; - mask[i] = 0; - } else { - filter[i] = (byte) (typeCode & (0xff >> (i - startOffset) * 8)); - mask[i] = (byte) 0xff; - } - } - scanFilters.add(new ScanFilter.Builder().setManufacturerData((int) manufacturer, filter, mask).build()); + for (BeaconParser beaconParser: beaconParsers) { + List sfds = createScanFilterDataForBeaconParser(beaconParser); + for (ScanFilterData sfd: sfds) { + ScanFilter.Builder builder = new ScanFilter.Builder(); + builder.setManufacturerData((int) sfd.manufacturer, sfd.filter, sfd.mask); + ScanFilter scanFilter = builder.build(); + scanFilters.add(scanFilter); } } return scanFilters; diff --git a/src/main/java/org/altbeacon/beacon/service/DetectionTracker.java b/src/main/java/org/altbeacon/beacon/service/DetectionTracker.java new file mode 100644 index 000000000..685f48014 --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/service/DetectionTracker.java @@ -0,0 +1,24 @@ +package org.altbeacon.beacon.service; + +/** + * Created by dyoung on 1/10/15. + */ +public class DetectionTracker { + private static DetectionTracker sDetectionTracker = null; + private long mLastDetectionTime = 0l; + private DetectionTracker() { + + } + public static synchronized DetectionTracker getInstance() { + if (sDetectionTracker == null) { + sDetectionTracker = new DetectionTracker(); + } + return sDetectionTracker; + } + public long getLastDetectionTime() { + return mLastDetectionTime; + } + public void recordDetection() { + mLastDetectionTime = System.currentTimeMillis(); + } +} diff --git a/src/test/AndroidManifest.xml b/src/test/AndroidManifest.xml new file mode 100644 index 000000000..a885a5559 --- /dev/null +++ b/src/test/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java b/src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java new file mode 100644 index 000000000..e31ada14a --- /dev/null +++ b/src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java @@ -0,0 +1,91 @@ +package org.altbeacon.beacon.service; + + +import android.bluetooth.le.ScanFilter; +import android.content.Context; + +import org.altbeacon.beacon.AltBeaconParser; +import org.altbeacon.beacon.Beacon; +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.BeaconParser; +import org.altbeacon.bluetooth.BluetoothCrashResolver; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Config(emulateSdk = 18) + +@RunWith(RobolectricTestRunner.class) +public class CycledLeScannerTest { + + + @BeforeClass + public static void testSetup() { + } + + @AfterClass + public static void testCleanup() { + + } + + @Test + public void testGetAltBeaconScanFilter() throws Exception { + org.robolectric.shadows.ShadowLog.stream = System.err; + Context context = Robolectric.getShadowApplication().getApplicationContext(); + BeaconParser parser = new AltBeaconParser(); + BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric + CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); + List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); + CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + assertEquals("manufacturer should be right", 0x0118, sfd.manufacturer); + assertEquals("mask length should be right", 2, sfd.mask.length); + assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); + assertArrayEquals("filter should be right", new byte[] {(byte)0xbe, (byte)0xac}, sfd.filter); + } + @Test + public void testGenericScanFilter() throws Exception { + org.robolectric.shadows.ShadowLog.stream = System.err; + Context context = Robolectric.getShadowApplication().getApplicationContext(); + BeaconParser parser = new BeaconParser(); + parser.setBeaconLayout("m:2-3=1111,i:4-6,p:24-24"); + BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric + CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); + List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); + CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + assertEquals("manufacturer should be right", 0x004c, sfd.manufacturer); + assertEquals("mask length should be right", 2, sfd.mask.length); + assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); + assertArrayEquals("filter should be right", new byte[] {(byte)0x11, (byte)0x11}, sfd.filter); + } + @Test + public void testZeroOffsetScanFilter() throws Exception { + org.robolectric.shadows.ShadowLog.stream = System.err; + Context context = Robolectric.getShadowApplication().getApplicationContext(); + BeaconParser parser = new BeaconParser(); + parser.setBeaconLayout("m:0-3=11223344,i:4-6,p:24-24"); + BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric + CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); + List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); + CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + assertEquals("manufacturer should be right", 0x004c, sfd.manufacturer); + assertEquals("mask length should be right", 2, sfd.mask.length); + assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); + assertArrayEquals("filter should be right", new byte[] {(byte)0x33, (byte)0x44}, sfd.filter); + } + +} From 6ed9c3c43bc4375829568d2362341485175f7a85 Mon Sep 17 00:00:00 2001 From: Mark van der Tol Date: Sun, 11 Jan 2015 22:52:39 +0100 Subject: [PATCH 4/6] Split old implementation and new Lollipop implementation for CycledLeScanner to prevent class load errors and improve readability. --- .../beacon/service/BeaconService.java | 11 +- .../beacon/service/CycledLeScanner.java | 312 +++++------------- .../CycledLeScannerForJellyBeanMr2.java | 87 +++++ .../service/CycledLeScannerForLollipop.java | 115 +++++++ 4 files changed, 284 insertions(+), 241 deletions(-) create mode 100644 src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java create mode 100644 src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java diff --git a/src/main/java/org/altbeacon/beacon/service/BeaconService.java b/src/main/java/org/altbeacon/beacon/service/BeaconService.java index 120f64068..b76311c67 100644 --- a/src/main/java/org/altbeacon/beacon/service/BeaconService.java +++ b/src/main/java/org/altbeacon/beacon/service/BeaconService.java @@ -23,14 +23,10 @@ */ package org.altbeacon.beacon.service; -import android.annotation.SuppressLint; + import android.annotation.TargetApi; import android.app.Service; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.ScanResult; -import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.os.AsyncTask; @@ -52,7 +48,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -69,13 +64,11 @@ public class BeaconService extends Service { private Map rangedRegionState = new HashMap(); private Map monitoredRegionState = new HashMap(); - private BluetoothAdapter bluetoothAdapter; private HashSet trackedBeacons; int trackedBeaconsPacketCount; private Handler handler = new Handler(); private int bindCount = 0; private BluetoothCrashResolver bluetoothCrashResolver; - private boolean scanCyclerStarted = false; private boolean scanningEnabled = false; private DistanceCalculator defaultDistanceCalculator = null; private List beaconParsers; @@ -201,7 +194,7 @@ public void onCreate() { Log.i(TAG, "beaconService version " + BuildConfig.VERSION_NAME + " is starting up"); bluetoothCrashResolver = new BluetoothCrashResolver(this); bluetoothCrashResolver.start(); - mCycledScanner = new CycledLeScanner(this, BeaconManager.DEFAULT_FOREGROUND_SCAN_PERIOD, + mCycledScanner = CycledLeScanner.createScanner(this, BeaconManager.DEFAULT_FOREGROUND_SCAN_PERIOD, BeaconManager.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD, mBackgroundFlag, mCycledLeScanCallback, bluetoothCrashResolver); beaconParsers = BeaconManager.getInstanceForApplication(getApplicationContext()).getBeaconParsers(); diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java index 205eb4260..0046b359b 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java @@ -5,75 +5,79 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; import android.content.Context; import android.content.Intent; import android.os.Handler; -import android.provider.AlarmClock; import android.util.Log; import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.startup.StartupBroadcastReceiver; import org.altbeacon.bluetooth.BluetoothCrashResolver; -import java.util.ArrayList; import java.util.Date; -import java.util.List; -/** - * Created by dyoung on 10/6/14. - */ -public class CycledLeScanner { +@TargetApi(18) +public abstract class CycledLeScanner { private static final String TAG = "CycledLeScanner"; private BluetoothAdapter mBluetoothAdapter; + private long mLastScanCycleStartTime = 0l; private long mLastScanCycleEndTime = 0l; - private long mNextScanCycleStartTime = 0l; + protected long mNextScanCycleStartTime = 0l; private long mScanCycleStopTime = 0l; + private boolean mScanning; - private boolean mScanningPaused; + protected boolean mScanningPaused; private boolean mScanCyclerStarted = false; private boolean mScanningEnabled = false; - private Context mContext; + private final Context mContext; private long mScanPeriod; - private long mBetweenScanPeriod; - private Handler mHandler = new Handler(); - private BluetoothCrashResolver mBluetoothCrashResolver; - private CycledLeScanCallback mCycledLeScanCallback; - private BluetoothLeScanner mScanner; - private boolean mUseAndroidLScanner = true; - private boolean mBackgroundFlag = false; - private boolean mRestartNeeded = false; - private long mWakeupAlarmTime = 0l; - - public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { + + protected long mBetweenScanPeriod; + protected final Handler mHandler = new Handler(); + + protected final BluetoothCrashResolver mBluetoothCrashResolver; + protected final CycledLeScanCallback mCycledLeScanCallback; + + protected boolean mBackgroundFlag = false; + protected boolean mRestartNeeded = false; + + protected CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { mScanPeriod = scanPeriod; mBetweenScanPeriod = betweenScanPeriod; mContext = context; mCycledLeScanCallback = cycledLeScanCallback; mBluetoothCrashResolver = crashResolver; mBackgroundFlag = backgroundFlag; + } + + public static CycledLeScanner createScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { + boolean useAndroidLScanner; + if (android.os.Build.VERSION.SDK_INT < 18) { + Log.w(TAG, "Not supported prior to API 18."); + return null; + } if (android.os.Build.VERSION.SDK_INT < 21) { Log.i(TAG, "This is not Android 5.0. We are using old scanning APIs"); - mUseAndroidLScanner = false; - } - else { + useAndroidLScanner = false; + } else { if (BeaconManager.isAndroidLScanningDisabled()) { Log.i(TAG, "This Android 5.0, but L scanning is disabled. We are using old scanning APIs"); - mUseAndroidLScanner = false; - } - else { + useAndroidLScanner = false; + } else { Log.i(TAG, "This Android 5.0. We are using new scanning APIs"); - mUseAndroidLScanner = true; + useAndroidLScanner = true; } } + if (useAndroidLScanner) { + return new CycledLeScannerForLollipop(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver); + } else { + return new CycledLeScannerForJellyBeanMr2(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver); + } + } /** @@ -83,18 +87,17 @@ public CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, * @param backgroundFlag */ public void setScanPeriods(long scanPeriod, long betweenScanPeriod, boolean backgroundFlag) { - BeaconManager.logDebug(TAG, "Set scan periods called with "+scanPeriod+", "+betweenScanPeriod+" Background mode must have changed."); + BeaconManager.logDebug(TAG, "Set scan periods called with " + scanPeriod + ", " + betweenScanPeriod + " Background mode must have changed."); if (mBackgroundFlag != backgroundFlag) { mRestartNeeded = true; } mBackgroundFlag = backgroundFlag; mScanPeriod = scanPeriod; mBetweenScanPeriod = betweenScanPeriod; - if (mBackgroundFlag == true) { + if (mBackgroundFlag) { BeaconManager.logDebug(TAG, "We are in the background. Setting wakeup alarm"); setWakeUpAlarm(); - } - else { + } else { BeaconManager.logDebug(TAG, "We are not in the background. Cancelling wakeup alarm"); cancelWakeUpAlarm(); } @@ -126,11 +129,11 @@ public void start() { mScanningEnabled = true; if (!mScanCyclerStarted) { scanLeDevice(true); - } - else { + } else { BeaconManager.logDebug(TAG, "scanning already started"); } } + @SuppressLint("NewApi") public void stop() { BeaconManager.logDebug(TAG, "stop called"); @@ -139,54 +142,20 @@ public void stop() { scanLeDevice(false); } if (mBluetoothAdapter != null) { - try { - if (mUseAndroidLScanner) { - mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); - } - else { - getBluetoothAdapter().stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback()); - } - } - catch (Exception e) { - Log.w("Internal Android exception scanning for beacons: ", e); - } + stopScan(); mLastScanCycleEndTime = new Date().getTime(); } - } - private boolean deferScanIfNeeded() { - if (mUseAndroidLScanner) { - // never stop scanning on Android L - return false; - } - long millisecondsUntilStart = mNextScanCycleStartTime - (new Date().getTime()); - if (millisecondsUntilStart > 0) { - BeaconManager.logDebug(TAG, "Waiting to start next bluetooth scan for another " + millisecondsUntilStart + " milliseconds"); - // Don't actually wait until the next scan time -- only wait up to 1 second. this - // allows us to start scanning sooner if a consumer enters the foreground and expects - // results more quickly - if (mBackgroundFlag) { - setWakeUpAlarm(); - } - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - scanLeDevice(true); - } - }, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart); - return true; - } - return false; - } + protected abstract void stopScan(); + + protected abstract boolean deferScanIfNeeded(); + + protected abstract void startScan(); @SuppressLint("NewApi") - private void scanLeDevice(final Boolean enable) { + protected void scanLeDevice(final Boolean enable) { mScanCyclerStarted = true; - if (android.os.Build.VERSION.SDK_INT < 18) { - Log.w(TAG, "Not supported prior to API 18."); - return; - } if (getBluetoothAdapter() == null) { Log.e(TAG, "No bluetooth adapter. beaconService cannot scan."); } @@ -195,7 +164,7 @@ private void scanLeDevice(final Boolean enable) { return; } BeaconManager.logDebug(TAG, "starting a new scan cycle"); - if (mScanning == false || mScanningPaused || mRestartNeeded) { + if (!mScanning || mScanningPaused || mRestartNeeded) { mScanning = true; mScanningPaused = false; try { @@ -203,49 +172,20 @@ private void scanLeDevice(final Boolean enable) { if (getBluetoothAdapter().isEnabled()) { if (mBluetoothCrashResolver != null && mBluetoothCrashResolver.isRecoveryInProgress()) { Log.w(TAG, "Skipping scan because crash recovery is in progress."); - } - else { + } else { if (mScanningEnabled) { - try { - if (mUseAndroidLScanner) { - if (mRestartNeeded){ - mRestartNeeded = false; - BeaconManager.logDebug(TAG, "restarting a bluetooth le scan"); - } - else { - BeaconManager.logDebug(TAG, "starting a new bluetooth le scan"); - } - List filters = new ArrayList(); - if (mScanner == null) { - BeaconManager.logDebug(TAG, "Making new Android L scanner"); - mScanner = getBluetoothAdapter().getBluetoothLeScanner(); - } - ScanSettings settings; - - if (mBackgroundFlag) { - BeaconManager.logDebug(TAG, "starting scan in SCAN_MODE_LOW_POWER"); - settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build(); - } - else { - BeaconManager.logDebug(TAG, "starting scan in SCAN_MODE_LOW_LATENCY"); - settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)).build(); - - } - mScanner.startScan(filters, settings, (android.bluetooth.le.ScanCallback) getNewLeScanCallback()); - - } - else { - BeaconManager.logDebug(TAG, "starting a new bluetooth le scan"); - mRestartNeeded = false; - getBluetoothAdapter().startLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback()); - } - + if (mRestartNeeded) { + mRestartNeeded = false; + BeaconManager.logDebug(TAG, "restarting a bluetooth le scan"); + } else { + BeaconManager.logDebug(TAG, "starting a new bluetooth le scan"); } - catch (Exception e) { + try { + startScan(); + } catch (Exception e) { Log.w("Internal Android exception scanning for beacons: ", e); } - } - else { + } else { BeaconManager.logDebug(TAG, "Scanning unnecessary - no monitoring or ranging active."); } } @@ -267,25 +207,13 @@ private void scanLeDevice(final Boolean enable) { } else { BeaconManager.logDebug(TAG, "disabling scan"); mScanning = false; - if (mUseAndroidLScanner) { - mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); - } - else { - if (getBluetoothAdapter() != null) { - try { - getBluetoothAdapter().stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback()); - } - catch (Exception e) { - Log.w("Internal Android exception scanning for beacons: ", e); - } - mLastScanCycleEndTime = new Date().getTime(); - } - } + stopScan(); + mLastScanCycleEndTime = new Date().getTime(); } } - private void scheduleScanCycleStop() { + protected void scheduleScanCycleStop() { // Stops scanning after a pre-defined scan period. long millisecondsUntilStop = mScanCycleStopTime - (new Date().getTime()); if (millisecondsUntilStop > 0) { @@ -304,40 +232,20 @@ public void run() { } } - @TargetApi(21) + protected abstract void finishScan(); + private void finishScanCycle() { BeaconManager.logDebug(TAG, "Done with scan cycle"); mCycledLeScanCallback.onCycleEnd(); - if (mScanning == true) { + if (mScanning) { if (getBluetoothAdapter() != null) { if (getBluetoothAdapter().isEnabled()) { try { BeaconManager.logDebug(TAG, "stopping bluetooth le scan"); - if (mUseAndroidLScanner) { - if (mBetweenScanPeriod == 0) { - // Prior to Android L we had to stop scanning at the end of each - // cycle, even the betweenScanPeriod was set to zero, and then - // immediately restart. This is because on the old APIS, connectable - // advertisements only were passed along to the callback the first - // time seen in a scan period. This is no longer true with the new - // Android L apis. All advertisements are passed along even for - // connectable advertisements. So there is no need to stop scanning - // if we are just going to start back up again. - BeaconManager.logDebug(TAG, "Aborting stop scan because this is Android L"); - } - else { - mScanner.stopScan((android.bluetooth.le.ScanCallback) getNewLeScanCallback()); - mScanningPaused = true; - } - } - else { - // Yes, this is deprecated as of API21. But we still use it for devices - // With API 18-20 - getBluetoothAdapter().stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback()); - mScanningPaused = true; - } - } - catch (Exception e) { + + finishScan(); + + } catch (Exception e) { Log.w("Internal Android exception scanning for beacons: ", e); } mLastScanCycleEndTime = new Date().getTime(); @@ -349,8 +257,7 @@ private void finishScanCycle() { mNextScanCycleStartTime = (new Date().getTime() + mBetweenScanPeriod); if (mScanningEnabled) { scanLeDevice(true); - } - else { + } else { BeaconManager.logDebug(TAG, "Scanning disabled. No ranging or monitoring regions are active."); mScanCyclerStarted = false; cancelWakeUpAlarm(); @@ -358,65 +265,7 @@ private void finishScanCycle() { } } - private Object leScanCallback; - @TargetApi(18) - private Object getLeScanCallback() { - if (leScanCallback == null) { - leScanCallback = - new BluetoothAdapter.LeScanCallback() { - - @Override - public void onLeScan(final BluetoothDevice device, final int rssi, - final byte[] scanRecord) { - BeaconManager.logDebug(TAG, "got record"); - mCycledLeScanCallback.onLeScan(device, rssi, scanRecord); - mBluetoothCrashResolver.notifyScannedDevice(device, (BluetoothAdapter.LeScanCallback) getLeScanCallback()); - } - }; - } - return leScanCallback; - } - - @TargetApi(21) - private Object getNewLeScanCallback() { - if (leScanCallback == null) { - leScanCallback = new android.bluetooth.le.ScanCallback() { - - @Override - public void onScanResult(int callbackType, ScanResult scanResult) { - // callback type - // Determines how this callback was triggered. Currently could only be - // CALLBACK_TYPE_ALL_MATCHES - BeaconManager.logDebug(TAG, "got record"); - mCycledLeScanCallback.onLeScan(scanResult.getDevice(), - scanResult.getRssi(), scanResult.getScanRecord().getBytes()); - // Don't call bluetoothcrashresolver on androidl. no need. - } - - @Override - public void onBatchScanResults (List results) { - BeaconManager.logDebug(TAG, "got batch records"); - for (ScanResult scanResult : results) { - mCycledLeScanCallback.onLeScan(scanResult.getDevice(), - scanResult.getRssi(), scanResult.getScanRecord().getBytes()); - } - } - - @Override - public void onScanFailed(int i) { - Log.e(TAG, "Scan Failed"); - } - }; - } - return leScanCallback; - } - - @TargetApi(18) - private BluetoothAdapter getBluetoothAdapter() { - if (android.os.Build.VERSION.SDK_INT < 18) { - Log.w(TAG, "Not supported prior to API 18."); - return null; - } + protected BluetoothAdapter getBluetoothAdapter() { if (mBluetoothAdapter == null) { // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = @@ -428,12 +277,12 @@ private BluetoothAdapter getBluetoothAdapter() { private PendingIntent mWakeUpOperation = null; - // In case we go into deep sleep, we will set up a wakeup alarm when in the background to kickofÆ’ + + // In case we go into deep sleep, we will set up a wakeup alarm when in the background to kickoff // off the scan cycle again - @TargetApi(3) - private void setWakeUpAlarm() { + protected void setWakeUpAlarm() { // wake up time will be the maximum of 5 minutes, the scan period, the between scan period - long milliseconds = 1000l*60*5; /* five minutes */ + long milliseconds = 1000l * 60 * 5; /* five minutes */ if (milliseconds < mBetweenScanPeriod) { milliseconds = mBetweenScanPeriod; } @@ -446,13 +295,12 @@ private void setWakeUpAlarm() { intent.putExtra("wakeup", true); cancelWakeUpAlarm(); mWakeUpOperation = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, System.currentTimeMillis()+milliseconds, mWakeUpOperation); - BeaconManager.logDebug(TAG, "Set a wakeup alarm to go off in "+milliseconds+" ms: "+mWakeUpOperation); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, System.currentTimeMillis() + milliseconds, mWakeUpOperation); + BeaconManager.logDebug(TAG, "Set a wakeup alarm to go off in " + milliseconds + " ms: " + mWakeUpOperation); } - @TargetApi(3) - private void cancelWakeUpAlarm() { - BeaconManager.logDebug(TAG, "cancel wakeup alarm: "+mWakeUpOperation); + protected void cancelWakeUpAlarm() { + BeaconManager.logDebug(TAG, "cancel wakeup alarm: " + mWakeUpOperation); if (mWakeUpOperation != null) { AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(mWakeUpOperation); diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java new file mode 100644 index 000000000..8b6c257b0 --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java @@ -0,0 +1,87 @@ +package org.altbeacon.beacon.service; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.Log; + +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.bluetooth.BluetoothCrashResolver; + +import java.util.Date; + +@TargetApi(18) +public class CycledLeScannerForJellyBeanMr2 extends CycledLeScanner { + private static final String TAG = "CycledLeScannerForJellyBeanMr2"; + private BluetoothAdapter.LeScanCallback leScanCallback; + + public CycledLeScannerForJellyBeanMr2(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { + super(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver); + } + + @SuppressWarnings("deprecation") + @Override + protected void stopScan() { + try { + BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(); + if (bluetoothAdapter != null) { + bluetoothAdapter.stopLeScan(getLeScanCallback()); + } + } catch (Exception e) { + Log.w("Internal Android exception scanning for beacons: ", e); + } + } + + @Override + protected boolean deferScanIfNeeded() { + long millisecondsUntilStart = mNextScanCycleStartTime - (new Date().getTime()); + if (millisecondsUntilStart > 0) { + BeaconManager.logDebug(TAG, "Waiting to start next bluetooth scan for another " + millisecondsUntilStart + " milliseconds"); + // Don't actually wait until the next scan time -- only wait up to 1 second. this + // allows us to start scanning sooner if a consumer enters the foreground and expects + // results more quickly + if (mBackgroundFlag) { + setWakeUpAlarm(); + } + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + scanLeDevice(true); + } + }, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart); + return true; + } + return false; + } + + @SuppressWarnings("deprecation") + @Override + protected void startScan() { + getBluetoothAdapter().startLeScan(getLeScanCallback()); + } + + @SuppressWarnings("deprecation") + @Override + protected void finishScan() { + getBluetoothAdapter().stopLeScan(getLeScanCallback()); + mScanningPaused = true; + } + + private BluetoothAdapter.LeScanCallback getLeScanCallback() { + if (leScanCallback == null) { + leScanCallback = + new BluetoothAdapter.LeScanCallback() { + + @Override + public void onLeScan(final BluetoothDevice device, final int rssi, + final byte[] scanRecord) { + BeaconManager.logDebug(TAG, "got record"); + mCycledLeScanCallback.onLeScan(device, rssi, scanRecord); + mBluetoothCrashResolver.notifyScannedDevice(device, getLeScanCallback()); + } + }; + } + return leScanCallback; + } +} diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java b/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java new file mode 100644 index 000000000..e92ac4339 --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java @@ -0,0 +1,115 @@ +package org.altbeacon.beacon.service; + +import android.annotation.TargetApi; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; +import android.util.Log; + +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.bluetooth.BluetoothCrashResolver; + +import java.util.ArrayList; +import java.util.List; + +@TargetApi(21) +public class CycledLeScannerForLollipop extends CycledLeScanner { + private static final String TAG = "CycledLeScannerForLollipop"; + private BluetoothLeScanner mScanner; + private ScanCallback leScanCallback; + + public CycledLeScannerForLollipop(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { + super(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver); + } + + @Override + protected void stopScan() { + try { + mScanner.stopScan(getNewLeScanCallback()); + } + catch (Exception e) { + Log.w("Internal Android exception scanning for beacons: ", e); + } + } + + @Override + protected boolean deferScanIfNeeded() { + // never stop scanning on Android L + return false; + } + + @Override + protected void startScan() { + List filters = new ArrayList(); + if (mScanner == null) { + BeaconManager.logDebug(TAG, "Making new Android L scanner"); + mScanner = getBluetoothAdapter().getBluetoothLeScanner(); + } + ScanSettings settings; + + if (mBackgroundFlag) { + BeaconManager.logDebug(TAG, "starting scan in SCAN_MODE_LOW_POWER"); + settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build(); + } else { + BeaconManager.logDebug(TAG, "starting scan in SCAN_MODE_LOW_LATENCY"); + settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)).build(); + + } + mScanner.startScan(filters, settings, getNewLeScanCallback()); + + } + + @Override + protected void finishScan() { + if (mBetweenScanPeriod == 0) { + // Prior to Android L we had to stop scanning at the end of each + // cycle, even the betweenScanPeriod was set to zero, and then + // immediately restart. This is because on the old APIS, connectable + // advertisements only were passed along to the callback the first + // time seen in a scan period. This is no longer true with the new + // Android L apis. All advertisements are passed along even for + // connectable advertisements. So there is no need to stop scanning + // if we are just going to start back up again. + BeaconManager.logDebug(TAG, "Aborting stop scan because this is Android L"); + } else { + mScanner.stopScan(getNewLeScanCallback()); + mScanningPaused = true; + } + } + + private ScanCallback getNewLeScanCallback() { + if (leScanCallback == null) { + leScanCallback = new ScanCallback() { + + @Override + public void onScanResult(int callbackType, ScanResult scanResult) { + // callback type + // Determines how this callback was triggered. Currently could only be + // CALLBACK_TYPE_ALL_MATCHES + BeaconManager.logDebug(TAG, "got record"); + mCycledLeScanCallback.onLeScan(scanResult.getDevice(), + scanResult.getRssi(), scanResult.getScanRecord().getBytes()); + // Don't call bluetoothcrashresolver on androidl. no need. + } + + @Override + public void onBatchScanResults(List results) { + BeaconManager.logDebug(TAG, "got batch records"); + for (ScanResult scanResult : results) { + mCycledLeScanCallback.onLeScan(scanResult.getDevice(), + scanResult.getRssi(), scanResult.getScanRecord().getBytes()); + } + } + + @Override + public void onScanFailed(int i) { + Log.e(TAG, "Scan Failed"); + } + }; + } + return leScanCallback; + } +} From 7570b33f2cbb081cef079c8189e17109f44e41fc Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Mon, 12 Jan 2015 18:30:15 -0500 Subject: [PATCH 5/6] Move scanning classes to subpackage --- .../beacon/service/BeaconService.java | 2 + .../{ => scanner}/CycledLeScanCallback.java | 2 +- .../{ => scanner}/CycledLeScanner.java | 22 +++++- .../CycledLeScannerForJellyBeanMr2.java | 2 +- .../CycledLeScannerForLollipop.java | 62 +--------------- .../service/scanner/ScanFilterUtils.java | 70 +++++++++++++++++++ .../ScanFilterUtilsTest.java} | 27 +++---- 7 files changed, 107 insertions(+), 80 deletions(-) rename src/main/java/org/altbeacon/beacon/service/{ => scanner}/CycledLeScanCallback.java (83%) rename src/main/java/org/altbeacon/beacon/service/{ => scanner}/CycledLeScanner.java (90%) rename src/main/java/org/altbeacon/beacon/service/{ => scanner}/CycledLeScannerForJellyBeanMr2.java (98%) rename src/main/java/org/altbeacon/beacon/service/{ => scanner}/CycledLeScannerForLollipop.java (80%) create mode 100644 src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java rename src/test/java/org/altbeacon/beacon/service/{CycledLeScannerTest.java => scanner/ScanFilterUtilsTest.java} (69%) diff --git a/src/main/java/org/altbeacon/beacon/service/BeaconService.java b/src/main/java/org/altbeacon/beacon/service/BeaconService.java index 7ae1a459f..15ddb3ca0 100644 --- a/src/main/java/org/altbeacon/beacon/service/BeaconService.java +++ b/src/main/java/org/altbeacon/beacon/service/BeaconService.java @@ -41,6 +41,8 @@ import org.altbeacon.beacon.BeaconParser; import org.altbeacon.beacon.distance.DistanceCalculator; import org.altbeacon.beacon.distance.ModelSpecificDistanceCalculator; +import org.altbeacon.beacon.service.scanner.CycledLeScanCallback; +import org.altbeacon.beacon.service.scanner.CycledLeScanner; import org.altbeacon.bluetooth.BluetoothCrashResolver; import org.altbeacon.beacon.BuildConfig; import org.altbeacon.beacon.Region; diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanCallback.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanCallback.java similarity index 83% rename from src/main/java/org/altbeacon/beacon/service/CycledLeScanCallback.java rename to src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanCallback.java index 884ac509f..3fe8904c8 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanCallback.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanCallback.java @@ -1,4 +1,4 @@ -package org.altbeacon.beacon.service; +package org.altbeacon.beacon.service.scanner; import android.bluetooth.BluetoothDevice; diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java similarity index 90% rename from src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java rename to src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java index 196ce6163..2ab1f470e 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java @@ -1,4 +1,4 @@ -package org.altbeacon.beacon.service; +package org.altbeacon.beacon.service.scanner; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -306,4 +306,24 @@ protected void cancelWakeUpAlarm() { alarmManager.cancel(mWakeUpOperation); } } + + private long getNextScanStartTime() { + // Because many apps may use this library on the same device, we want to try to synchonize + // scanning as much as possible in order to save battery. Therefore, we will set the scan + // intervals to be on a predictable interval using a modulus of the system time. This may + // cause scans to start a little earlier than otherwise, but it should be acceptable. + // This way, if multiple apps on the device are using the default scan periods, then they + // will all be doing scans at the same time, thereby saving battery when none are scanning. + // This, of course, won't help at all if people set custom scan periods. But since most + // people accept the defaults, this will likely have a positive effect. + if (mBetweenScanPeriod == 0) { + return System.currentTimeMillis(); + } + long fullScanCycle = mScanPeriod + mBetweenScanPeriod; + long normalizedBetweenScanPeriod = mBetweenScanPeriod-(System.currentTimeMillis() % fullScanCycle); + BeaconManager.logDebug(TAG, "Normalizing between scan period from "+mBetweenScanPeriod+" to "+ + normalizedBetweenScanPeriod); + + return System.currentTimeMillis()+normalizedBetweenScanPeriod; + } } \ No newline at end of file diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForJellyBeanMr2.java similarity index 98% rename from src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java rename to src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForJellyBeanMr2.java index 8b6c257b0..6e91ba7d7 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForJellyBeanMr2.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForJellyBeanMr2.java @@ -1,4 +1,4 @@ -package org.altbeacon.beacon.service; +package org.altbeacon.beacon.service.scanner; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; diff --git a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java similarity index 80% rename from src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java rename to src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java index 5889bcbef..6ec7402c3 100644 --- a/src/main/java/org/altbeacon/beacon/service/CycledLeScannerForLollipop.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java @@ -1,4 +1,4 @@ -package org.altbeacon.beacon.service; +package org.altbeacon.beacon.service.scanner; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -12,6 +12,7 @@ import org.altbeacon.beacon.BeaconParser; import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.service.DetectionTracker; import org.altbeacon.bluetooth.BluetoothCrashResolver; import java.util.ArrayList; @@ -95,7 +96,7 @@ protected boolean deferScanIfNeeded() { // if we see one of those beacons, we need to deliver the results ScanSettings settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build(); - mScanner.startScan(createScanFiltersForBeaconParsers(mBeaconManager.getBeaconParsers()), settings, + mScanner.startScan(new ScanFilterUtils().createScanFiltersForBeaconParsers(mBeaconManager.getBeaconParsers()), settings, (android.bluetooth.le.ScanCallback) getNewLeScanCallback()); } else { @@ -215,61 +216,4 @@ public void onScanFailed(int i) { } return leScanCallback; } - - class ScanFilterData { - public int manufacturer; - public byte[] filter; - public byte[] mask; - } - - protected List createScanFilterDataForBeaconParser(BeaconParser beaconParser) { - ArrayList scanFilters = new ArrayList(); - for (int manufacturer : beaconParser.getHardwareAssistManufacturers()) { - long typeCode = beaconParser.getMatchingBeaconTypeCode(); - int startOffset = beaconParser.getMatchingBeaconTypeCodeStartOffset(); - int endOffset = beaconParser.getMatchingBeaconTypeCodeEndOffset(); - - // Note: the -2 here is because we want the filter and mask to start after the - // two-byte manufacturer code, and the beacon parser expression is based on offsets - // from the start of the two byte code - byte[] filter = new byte[endOffset + 1 - 2]; - byte[] mask = new byte[endOffset + 1 - 2]; - byte[] typeCodeBytes = BeaconParser.longToByteArray(typeCode, endOffset-startOffset+1); - for (int layoutIndex = 2; layoutIndex <= endOffset; layoutIndex++) { - int filterIndex = layoutIndex-2; - if (layoutIndex < startOffset) { - filter[filterIndex] = 0; - mask[filterIndex] = 0; - } else { - filter[filterIndex] = typeCodeBytes[layoutIndex-startOffset]; - mask[filterIndex] = (byte) 0xff; - } - } - ScanFilterData sfd = new ScanFilterData(); - sfd.manufacturer = manufacturer; - sfd.filter = filter; - sfd.mask = mask; - scanFilters.add(sfd); - - } - return scanFilters; - } - - - @TargetApi(21) - protected List createScanFiltersForBeaconParsers(List beaconParsers) { - List scanFilters = new ArrayList(); - // for each beacon parser, make a filter expression that includes all its desired - // hardware manufacturers - for (BeaconParser beaconParser: beaconParsers) { - List sfds = createScanFilterDataForBeaconParser(beaconParser); - for (ScanFilterData sfd: sfds) { - ScanFilter.Builder builder = new ScanFilter.Builder(); - builder.setManufacturerData((int) sfd.manufacturer, sfd.filter, sfd.mask); - ScanFilter scanFilter = builder.build(); - scanFilters.add(scanFilter); - } - } - return scanFilters; - } } diff --git a/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java b/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java new file mode 100644 index 000000000..c7700da1a --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/service/scanner/ScanFilterUtils.java @@ -0,0 +1,70 @@ +package org.altbeacon.beacon.service.scanner; + +import android.annotation.TargetApi; +import android.bluetooth.le.ScanFilter; + +import org.altbeacon.beacon.BeaconParser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by dyoung on 1/12/15. + */ +@TargetApi(21) +public class ScanFilterUtils { + class ScanFilterData { + public int manufacturer; + public byte[] filter; + public byte[] mask; + } + + public List createScanFilterDataForBeaconParser(BeaconParser beaconParser) { + ArrayList scanFilters = new ArrayList(); + for (int manufacturer : beaconParser.getHardwareAssistManufacturers()) { + long typeCode = beaconParser.getMatchingBeaconTypeCode(); + int startOffset = beaconParser.getMatchingBeaconTypeCodeStartOffset(); + int endOffset = beaconParser.getMatchingBeaconTypeCodeEndOffset(); + + // Note: the -2 here is because we want the filter and mask to start after the + // two-byte manufacturer code, and the beacon parser expression is based on offsets + // from the start of the two byte code + byte[] filter = new byte[endOffset + 1 - 2]; + byte[] mask = new byte[endOffset + 1 - 2]; + byte[] typeCodeBytes = BeaconParser.longToByteArray(typeCode, endOffset-startOffset+1); + for (int layoutIndex = 2; layoutIndex <= endOffset; layoutIndex++) { + int filterIndex = layoutIndex-2; + if (layoutIndex < startOffset) { + filter[filterIndex] = 0; + mask[filterIndex] = 0; + } else { + filter[filterIndex] = typeCodeBytes[layoutIndex-startOffset]; + mask[filterIndex] = (byte) 0xff; + } + } + ScanFilterData sfd = new ScanFilterData(); + sfd.manufacturer = manufacturer; + sfd.filter = filter; + sfd.mask = mask; + scanFilters.add(sfd); + + } + return scanFilters; + } + + public List createScanFiltersForBeaconParsers(List beaconParsers) { + List scanFilters = new ArrayList(); + // for each beacon parser, make a filter expression that includes all its desired + // hardware manufacturers + for (BeaconParser beaconParser: beaconParsers) { + List sfds = createScanFilterDataForBeaconParser(beaconParser); + for (ScanFilterData sfd: sfds) { + ScanFilter.Builder builder = new ScanFilter.Builder(); + builder.setManufacturerData((int) sfd.manufacturer, sfd.filter, sfd.mask); + ScanFilter scanFilter = builder.build(); + scanFilters.add(scanFilter); + } + } + return scanFilters; + } +} diff --git a/src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java b/src/test/java/org/altbeacon/beacon/service/scanner/ScanFilterUtilsTest.java similarity index 69% rename from src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java rename to src/test/java/org/altbeacon/beacon/service/scanner/ScanFilterUtilsTest.java index e31ada14a..167756cf7 100644 --- a/src/test/java/org/altbeacon/beacon/service/CycledLeScannerTest.java +++ b/src/test/java/org/altbeacon/beacon/service/scanner/ScanFilterUtilsTest.java @@ -1,14 +1,12 @@ -package org.altbeacon.beacon.service; +package org.altbeacon.beacon.service.scanner; -import android.bluetooth.le.ScanFilter; import android.content.Context; import org.altbeacon.beacon.AltBeaconParser; -import org.altbeacon.beacon.Beacon; import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.BeaconParser; -import org.altbeacon.bluetooth.BluetoothCrashResolver; +import org.altbeacon.beacon.service.scanner.ScanFilterUtils; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -17,7 +15,6 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertArrayEquals; @@ -28,7 +25,7 @@ @Config(emulateSdk = 18) @RunWith(RobolectricTestRunner.class) -public class CycledLeScannerTest { +public class ScanFilterUtilsTest { @BeforeClass @@ -43,13 +40,11 @@ public static void testCleanup() { @Test public void testGetAltBeaconScanFilter() throws Exception { org.robolectric.shadows.ShadowLog.stream = System.err; - Context context = Robolectric.getShadowApplication().getApplicationContext(); BeaconParser parser = new AltBeaconParser(); BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric - CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); - List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + List scanFilterDatas = new ScanFilterUtils().createScanFilterDataForBeaconParser(parser); assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); - CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + ScanFilterUtils.ScanFilterData sfd = scanFilterDatas.get(0); assertEquals("manufacturer should be right", 0x0118, sfd.manufacturer); assertEquals("mask length should be right", 2, sfd.mask.length); assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); @@ -58,14 +53,12 @@ public void testGetAltBeaconScanFilter() throws Exception { @Test public void testGenericScanFilter() throws Exception { org.robolectric.shadows.ShadowLog.stream = System.err; - Context context = Robolectric.getShadowApplication().getApplicationContext(); BeaconParser parser = new BeaconParser(); parser.setBeaconLayout("m:2-3=1111,i:4-6,p:24-24"); BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric - CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); - List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + List scanFilterDatas = new ScanFilterUtils().createScanFilterDataForBeaconParser(parser); assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); - CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + ScanFilterUtils.ScanFilterData sfd = scanFilterDatas.get(0); assertEquals("manufacturer should be right", 0x004c, sfd.manufacturer); assertEquals("mask length should be right", 2, sfd.mask.length); assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); @@ -74,14 +67,12 @@ public void testGenericScanFilter() throws Exception { @Test public void testZeroOffsetScanFilter() throws Exception { org.robolectric.shadows.ShadowLog.stream = System.err; - Context context = Robolectric.getShadowApplication().getApplicationContext(); BeaconParser parser = new BeaconParser(); parser.setBeaconLayout("m:0-3=11223344,i:4-6,p:24-24"); BeaconManager.setsManifestCheckingDisabled(true); // no manifest available in robolectric - CycledLeScanner cycledLeScanner = new CycledLeScanner(context, 0l, 0l, false, null, null); - List scanFilterDatas = cycledLeScanner.createScanFilterDataForBeaconParser(parser); + List scanFilterDatas = new ScanFilterUtils().createScanFilterDataForBeaconParser(parser); assertEquals("scanFilters should be of correct size", 1, scanFilterDatas.size()); - CycledLeScanner.ScanFilterData sfd = scanFilterDatas.get(0); + ScanFilterUtils.ScanFilterData sfd = scanFilterDatas.get(0); assertEquals("manufacturer should be right", 0x004c, sfd.manufacturer); assertEquals("mask length should be right", 2, sfd.mask.length); assertArrayEquals("mask should be right", new byte[] {(byte)0xff, (byte)0xff}, sfd.mask); From 73895e7b452d8de4f91750eb9ad83071442989b1 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Mon, 12 Jan 2015 18:33:56 -0500 Subject: [PATCH 6/6] remove unneeded annotation --- .../beacon/service/scanner/CycledLeScannerForLollipop.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java index 6ec7402c3..e9d0a31b0 100644 --- a/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScannerForLollipop.java @@ -76,7 +76,6 @@ then no beacons will be detected until the next scan cycle starts (5 minutes max will get a callback within a few seconds on Android L vs. up to 5 minutes on older operating system versions. */ - @SuppressLint("NewApi") protected boolean deferScanIfNeeded() { long millisecondsUntilStart = mNextScanCycleStartTime - System.currentTimeMillis(); if (millisecondsUntilStart > 0) {