Skip to content

Commit 342bfff

Browse files
authored
Merge pull request #491 from AltBeacon/no-scanning-restarts-on-duplicate-detection-devices
Don't restart scanning if device detects duplicate advertisements per scan
2 parents ef90455 + 1e49cd6 commit 342bfff

File tree

5 files changed

+157
-19
lines changed

5 files changed

+157
-19
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
### Development
22

3+
Enhancements:
4+
5+
- Don't restart BLE scanning periodically if the library confrims device can detect duplicate
6+
advertisements in a single scan, leading to more reliable detections with short scan cycles
7+
(#491, David G. Young)
8+
39
Bug Fixes:
410

511
- Deprecate misspelled methods `removeMonitoreNotifier` and

src/main/java/org/altbeacon/beacon/service/BeaconService.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.altbeacon.beacon.logging.LogManager;
5151
import org.altbeacon.beacon.service.scanner.CycledLeScanCallback;
5252
import org.altbeacon.beacon.service.scanner.CycledLeScanner;
53+
import org.altbeacon.beacon.service.scanner.DistinctPacketDetector;
5354
import org.altbeacon.beacon.service.scanner.NonBeaconLeScanCallback;
5455
import org.altbeacon.beacon.startup.StartupBroadcastReceiver;
5556
import org.altbeacon.bluetooth.BluetoothCrashResolver;
@@ -90,7 +91,8 @@ public class BeaconService extends Service {
9091
private boolean mBackgroundFlag = false;
9192
private ExtraDataBeaconTracker mExtraDataBeaconTracker;
9293
private ExecutorService mExecutor;
93-
94+
private final DistinctPacketDetector mDistinctPacketDetector = new DistinctPacketDetector();
95+
9496
/*
9597
* The scan period is how long we wait between restarting the BLE advertisement scans
9698
* 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) {
359361

360362
@Override
361363
public void onCycleEnd() {
364+
mDistinctPacketDetector.clearDetections();
362365
monitoringStatus.updateNewlyOutside();
363366
processRangeData();
364367
// 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) {
479482
}
480483
if (beacon != null) {
481484
mDetectionTracker.recordDetection();
485+
if (!mCycledScanner.getDistinctPacketsDetectedPerScan()) {
486+
if (!mDistinctPacketDetector.isPacketDistinct(scanData.device.getAddress(),
487+
scanData.scanRecord)) {
488+
LogManager.i(TAG, "Non-distinct packets detected in a single scan. Restarting scans unecessary.");
489+
mCycledScanner.setDistinctPacketsDetectedPerScan(true);
490+
}
491+
}
482492
trackedBeaconsPacketCount++;
483493
processBeaconFromScan(beacon);
484494
} else {

src/main/java/org/altbeacon/beacon/service/scanner/CycledLeScanner.java

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import org.altbeacon.beacon.logging.LogManager;
2121
import org.altbeacon.beacon.startup.StartupBroadcastReceiver;
2222
import org.altbeacon.bluetooth.BluetoothCrashResolver;
23-
2423
import java.util.Date;
2524

2625
@TargetApi(18)
@@ -52,6 +51,7 @@ public abstract class CycledLeScanner {
5251
protected boolean mBackgroundFlag = false;
5352
protected boolean mRestartNeeded = false;
5453

54+
private boolean mDistinctPacketsDetectedPerScan = false;
5555
private static final long ANDROID_N_MIN_SCAN_CYCLE_MILLIS = 6000l;
5656

5757
protected CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) {
@@ -162,6 +162,14 @@ public void stop() {
162162
}
163163
}
164164

165+
public boolean getDistinctPacketsDetectedPerScan() {
166+
return mDistinctPacketsDetectedPerScan;
167+
}
168+
169+
public void setDistinctPacketsDetectedPerScan(boolean detected) {
170+
mDistinctPacketsDetectedPerScan = detected;
171+
}
172+
165173
public void destroy() {
166174
mScanThread.quit();
167175
}
@@ -268,25 +276,39 @@ private void finishScanCycle() {
268276
if (mScanning) {
269277
if (getBluetoothAdapter() != null) {
270278
if (getBluetoothAdapter().isEnabled()) {
271-
long now = SystemClock.elapsedRealtime();
272-
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
273-
mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS &&
274-
now-mLastScanCycleStartTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) {
275-
// As of Android N, only 5 scans may be started in a 30 second period (6
276-
// seconds per cycle) otherwise they are blocked. So we check here to see
277-
// if the scan period is 6 seconds or less, and if we last stopped scanning
278-
// fewer than 6 seconds ag and if so, we simply do not stop scanning
279-
LogManager.d(TAG, "Not stopping scan because this is Android N and we" +
280-
" keep scanning for a minimum of 6 seconds at a time. "+
281-
"We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanCycleStartTime))+" millisconds.");
279+
// Determine if we need to restart scanning. Restarting scanning is only
280+
// needed on devices incapable of detecting multiple distinct BLE advertising
281+
// packets in a single cycle, typically older Android devices (e.g. Nexus 4)
282+
// On such devices, it is necessary to stop scanning and restart to detect
283+
// multiple beacon packets in the same scan, allowing collection of multiple
284+
// rssi measurements. Restarting however, causes brief detection dropouts
285+
// so it is best avoided. If we know the device has detected to distinct
286+
// packets in the same cycle, we will not restart scanning and just keep it
287+
// going.
288+
if (!getDistinctPacketsDetectedPerScan()) {
289+
long now = SystemClock.elapsedRealtime();
290+
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
291+
mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS &&
292+
now-mLastScanCycleStartTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) {
293+
// As of Android N, only 5 scans may be started in a 30 second period (6
294+
// seconds per cycle) otherwise they are blocked. So we check here to see
295+
// if the scan period is 6 seconds or less, and if we last stopped scanning
296+
// fewer than 6 seconds ag and if so, we simply do not stop scanning
297+
LogManager.d(TAG, "Not stopping scan because this is Android N and we" +
298+
" keep scanning for a minimum of 6 seconds at a time. "+
299+
"We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanCycleStartTime))+" millisconds.");
300+
}
301+
else {
302+
try {
303+
LogManager.d(TAG, "stopping bluetooth le scan");
304+
finishScan();
305+
} catch (Exception e) {
306+
LogManager.w(e, TAG, "Internal Android exception scanning for beacons");
307+
}
308+
}
282309
}
283310
else {
284-
try {
285-
LogManager.d(TAG, "stopping bluetooth le scan");
286-
finishScan();
287-
} catch (Exception e) {
288-
LogManager.w(e, TAG, "Internal Android exception scanning for beacons");
289-
}
311+
LogManager.d(TAG, "Not stopping scanning. Device capable of multiple indistinct detections per scan.");
290312
}
291313

292314
mLastScanCycleEndTime = SystemClock.elapsedRealtime();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.altbeacon.beacon.service.scanner;
2+
3+
import android.util.Log;
4+
5+
import java.nio.ByteBuffer;
6+
import java.util.HashSet;
7+
import java.util.Set;
8+
9+
/**
10+
* Created by dyoung on 4/8/17.
11+
*
12+
* This class tracks whether multiple distinct BLE packets have been seen, with the purpose of
13+
* determining if the Android device supports detecting multiple distinct packets in a single scan.
14+
* Some older devices are not capable of this (e.g. Nexus 4, Moto G1), so detecting multiple packets
15+
* requires stopping and restarting scanning on these devices. This allows detecting if that is
16+
* neessary
17+
*/
18+
public class DistinctPacketDetector {
19+
// Sanity limit for the number of packets to track, so we don't use too much memory
20+
private static final int MAX_PACKETS_TO_TRACK = 1000;
21+
protected Set<ByteBuffer> mDistinctPacketsDetected = new HashSet<ByteBuffer>();
22+
23+
public void clearDetections() {
24+
mDistinctPacketsDetected.clear();
25+
}
26+
27+
public boolean isPacketDistinct(String originMacAddress, byte[] scanRecord) {
28+
byte[] macBytes = originMacAddress.getBytes();
29+
ByteBuffer buffer = ByteBuffer.allocate(macBytes.length+scanRecord.length);
30+
buffer.put(macBytes);
31+
buffer.put(scanRecord);
32+
buffer.rewind(); // rewind puts position back to beginning so .equals and .hashCode work
33+
34+
boolean distinct = !mDistinctPacketsDetected.contains(buffer);
35+
if (mDistinctPacketsDetected.size() == MAX_PACKETS_TO_TRACK) {
36+
return mDistinctPacketsDetected.contains(buffer);
37+
}
38+
else {
39+
return mDistinctPacketsDetected.add(buffer);
40+
}
41+
}
42+
43+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.altbeacon.beacon.service.scanner;
2+
import org.junit.AfterClass;
3+
import org.junit.BeforeClass;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.robolectric.RobolectricTestRunner;
7+
import org.robolectric.annotation.Config;
8+
import static org.junit.Assert.assertFalse;
9+
import static org.junit.Assert.assertTrue;
10+
11+
@Config(sdk = 18)
12+
13+
@RunWith(RobolectricTestRunner.class)
14+
public class DistinctPacketDetectorTest {
15+
@BeforeClass
16+
public static void testSetup() {
17+
}
18+
19+
@AfterClass
20+
public static void testCleanup() {
21+
22+
}
23+
24+
@Test
25+
public void testSecondDuplicatePacketIsNotDistinct() throws Exception {
26+
DistinctPacketDetector dpd = new DistinctPacketDetector();
27+
dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
28+
boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
29+
assertFalse("second call with same packet should not be distinct", secondResult);
30+
}
31+
32+
@Test
33+
public void testSecondNonDuplicatePacketIsDistinct() throws Exception {
34+
DistinctPacketDetector dpd = new DistinctPacketDetector();
35+
dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
36+
boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x03, 0x04});
37+
assertTrue("second call with different packet should be distinct", secondResult);
38+
}
39+
40+
@Test
41+
public void testSamePacketForDifferentMacIsDistinct() throws Exception {
42+
DistinctPacketDetector dpd = new DistinctPacketDetector();
43+
dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
44+
boolean secondResult = dpd.isPacketDistinct("01:01:01:01:01:01", new byte[] {0x01, 0x02});
45+
assertTrue("second packet with different mac should be distinct", secondResult);
46+
}
47+
48+
@Test
49+
public void clearingDetectionsPreventsDistinctDetection() throws Exception {
50+
DistinctPacketDetector dpd = new DistinctPacketDetector();
51+
dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
52+
dpd.clearDetections();
53+
boolean secondResult = dpd.isPacketDistinct("01:02:03:04:05:06", new byte[] {0x01, 0x02});
54+
assertTrue("second call with same packet after clear should be distinct", secondResult);
55+
}
56+
57+
}

0 commit comments

Comments
 (0)