From 15a723f0fcd7533395d4a49440487a3d05106ec7 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sat, 8 Apr 2017 14:51:30 -0400 Subject: [PATCH 1/4] Track distinct packet detections per cycle and don't stop scanning if we see duplicates. --- .../beacon/service/BeaconService.java | 12 +++- .../service/scanner/CycledLeScanner.java | 60 +++++++++++++------ .../scanner/DistinctPacketDetector.java | 43 +++++++++++++ .../scanner/DistinctPacketDetectorTest.java | 57 ++++++++++++++++++ 4 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetector.java create mode 100644 src/test/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetectorTest.java diff --git a/src/main/java/org/altbeacon/beacon/service/BeaconService.java b/src/main/java/org/altbeacon/beacon/service/BeaconService.java index ea2f406b8..0e8aecf24 100644 --- a/src/main/java/org/altbeacon/beacon/service/BeaconService.java +++ b/src/main/java/org/altbeacon/beacon/service/BeaconService.java @@ -50,6 +50,7 @@ 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.bluetooth.BluetoothCrashResolver; @@ -90,7 +91,8 @@ public class BeaconService extends Service { 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) @@ -359,6 +361,7 @@ public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { @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 @@ -479,6 +482,13 @@ protected Void doInBackground(ScanData... params) { } if (beacon != null) { 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 { diff --git a/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java index 4d370d55e..f3e5b1a28 100644 --- a/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java +++ b/src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java @@ -20,7 +20,6 @@ import org.altbeacon.beacon.logging.LogManager; import org.altbeacon.beacon.startup.StartupBroadcastReceiver; import org.altbeacon.bluetooth.BluetoothCrashResolver; - import java.util.Date; @TargetApi(18) @@ -53,6 +52,7 @@ public abstract class CycledLeScanner { protected boolean mBackgroundFlag = false; protected boolean mRestartNeeded = false; + private boolean mDistinctPacketsDetectedPerScan = false; private static final long ANDROID_N_MIN_SCAN_CYCLE_MILLIS = 6000l; protected CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) { @@ -165,6 +165,14 @@ public void stop() { } } + public boolean getDistinctPacketsDetectedPerScan() { + return mDistinctPacketsDetectedPerScan; + } + + public void setDistinctPacketsDetectedPerScan(boolean detected) { + mDistinctPacketsDetectedPerScan = detected; + } + public void destroy() { mScanThread.quit(); } @@ -271,26 +279,40 @@ private void finishScanCycle() { if (mScanning) { if (getBluetoothAdapter() != null) { if (getBluetoothAdapter().isEnabled()) { - long now = System.currentTimeMillis(); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS && - now-mLastScanStopTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) { - // As of Android N, only 5 scans may be started in a 30 second period (6 - // seconds per cycle) otherwise they are blocked. So we check here to see - // if the scan period is 6 seconds or less, and if we last stopped scanning - // fewer than 6 seconds ag and if so, we simply do not stop scanning - LogManager.d(TAG, "Not stopping scan because this is Android N and we" + - " keep scanning for a minimum of 6 seconds at a time. "+ - "We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanStopTime))+" millisconds."); + // Determine if we need to restart scanning. Restarting scanning is only + // needed on devices incapable of detecting multiple distinct BLE advertising + // packets in a single cycle, typically older Android devices (e.g. Nexus 4) + // On such devices, it is necessary to stop scanning and restart to detect + // multiple beacon packets in the same scan, allowing collection of multiple + // rssi measurements. Restarting however, causes brief detection dropouts + // so it is best avoided. If we know the device has detected to distinct + // packets in the same cycle, we will not restart scanning and just keep it + // going. + if (!getDistinctPacketsDetectedPerScan()) { + long now = System.currentTimeMillis(); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS && + now-mLastScanStopTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) { + // As of Android N, only 5 scans may be started in a 30 second period (6 + // seconds per cycle) otherwise they are blocked. So we check here to see + // if the scan period is 6 seconds or less, and if we last stopped scanning + // fewer than 6 seconds ag and if so, we simply do not stop scanning + LogManager.d(TAG, "Not stopping scan because this is Android N and we" + + " keep scanning for a minimum of 6 seconds at a time. "+ + "We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanStopTime))+" millisconds."); + } + else { + try { + LogManager.d(TAG, "stopping bluetooth le scan"); + finishScan(); + mLastScanStopTime = now; + } catch (Exception e) { + LogManager.w(e, TAG, "Internal Android exception scanning for beacons"); + } + } } else { - try { - LogManager.d(TAG, "stopping bluetooth le scan"); - finishScan(); - mLastScanStopTime = now; - } catch (Exception e) { - LogManager.w(e, TAG, "Internal Android exception scanning for beacons"); - } + LogManager.d(TAG, "Not stopping scanning. Device capable of multiple indistinct detections per scan."); } mLastScanCycleEndTime = SystemClock.elapsedRealtime(); diff --git a/src/main/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetector.java b/src/main/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetector.java new file mode 100644 index 000000000..42978d30f --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetector.java @@ -0,0 +1,43 @@ +package org.altbeacon.beacon.service.scanner; + +import android.util.Log; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by dyoung on 4/8/17. + * + * This class tracks whether multiple distinct BLE packets have been seen, with the purpose of + * determining if the Android device supports detecting multiple distinct packets in a single scan. + * Some older devices are not capable of this (e.g. Nexus 4, Moto G1), so detecting multiple packets + * requires stopping and restarting scanning on these devices. This allows detecting if that is + * neessary + */ +public class DistinctPacketDetector { + // Sanity limit for the number of packets to track, so we don't use too much memory + private static final int MAX_PACKETS_TO_TRACK = 1000; + protected Set mDistinctPacketsDetected = new HashSet(); + + public void clearDetections() { + mDistinctPacketsDetected.clear(); + } + + public boolean isPacketDistinct(String originMacAddress, byte[] scanRecord) { + byte[] macBytes = originMacAddress.getBytes(); + ByteBuffer buffer = ByteBuffer.allocate(macBytes.length+scanRecord.length); + buffer.put(macBytes); + buffer.put(scanRecord); + buffer.rewind(); // rewind puts position back to beginning so .equals and .hashCode work + + boolean distinct = !mDistinctPacketsDetected.contains(buffer); + if (mDistinctPacketsDetected.size() == MAX_PACKETS_TO_TRACK) { + return mDistinctPacketsDetected.contains(buffer); + } + else { + return mDistinctPacketsDetected.add(buffer); + } + } + +} diff --git a/src/test/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetectorTest.java b/src/test/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetectorTest.java new file mode 100644 index 000000000..f82e152c8 --- /dev/null +++ b/src/test/java/org/altbeacon/beacon/service/scanner/DistinctPacketDetectorTest.java @@ -0,0 +1,57 @@ +package org.altbeacon.beacon.service.scanner; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@Config(sdk = 18) + +@RunWith(RobolectricTestRunner.class) +public class DistinctPacketDetectorTest { + @BeforeClass + public static void testSetup() { + } + + @AfterClass + public static void testCleanup() { + + } + + @Test + public void testSecondDuplicatePacketIsNotDistinct() throws Exception { + DistinctPacketDetector dpd = new DistinctPacketDetector(); + dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + assertFalse("second call with same packet should not be distinct", secondResult); + } + + @Test + public void testSecondNonDuplicatePacketIsDistinct() throws Exception { + DistinctPacketDetector dpd = new DistinctPacketDetector(); + dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x03, 0x04}); + assertTrue("second call with different packet should be distinct", secondResult); + } + + @Test + public void testSamePacketForDifferentMacIsDistinct() throws Exception { + DistinctPacketDetector dpd = new DistinctPacketDetector(); + dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + boolean secondResult = dpd.isPacketDistinct("01:01:01:01:01:01", new byte[] {0x01, 0x02}); + assertTrue("second packet with different mac should be distinct", secondResult); + } + + @Test + public void clearingDetectionsPreventsDistinctDetection() throws Exception { + DistinctPacketDetector dpd = new DistinctPacketDetector(); + dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + dpd.clearDetections(); + boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02}); + assertTrue("second call with same packet after clear should be distinct", secondResult); + } + +} \ No newline at end of file From 1628a562e5e8b1ac9e9768e61488fb596eba3258 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sat, 8 Apr 2017 15:11:54 -0400 Subject: [PATCH 2/4] Add changelog entry --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec704e010..5eb4d1736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ ### Development +Enhancements: + +- Don't restart BLE scanning periodically if the library confrims device can detect duplicate + advertisements in a single scan, leading to more reliable detections with short scan cycles + (#456, David G. Young) + Bug Fixes: - Deprecate misspelled methods `removeMonitoreNotifier` and `setRegionStatePeristenceEnabled` in favor of correctly spelled alternatives. (#461, Marco Salis) - ### 2.9.2 / 2016-11-22 [Full Changelog](https://github.com/AltBeacon/android-beacon-library/compare/2.9.1...2.9.2) From 44592adf317344bc8ee7ba47a48d8625ccbf4c82 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sat, 8 Apr 2017 15:19:07 -0400 Subject: [PATCH 3/4] Whitespace cleanup --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb4d1736..70d8f5c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Bug Fixes: `setRegionStatePeristenceEnabled` in favor of correctly spelled alternatives. (#461, Marco Salis) + ### 2.9.2 / 2016-11-22 [Full Changelog](https://github.com/AltBeacon/android-beacon-library/compare/2.9.1...2.9.2) From c1dacc9e6c4a5024f52af2aa649e9148df26f452 Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Sat, 8 Apr 2017 15:20:59 -0400 Subject: [PATCH 4/4] Fix PR number in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d8f5c49..02d7869ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Enhancements: - Don't restart BLE scanning periodically if the library confrims device can detect duplicate advertisements in a single scan, leading to more reliable detections with short scan cycles - (#456, David G. Young) + (#491, David G. Young) Bug Fixes: