Skip to content

Commit 1e49cd6

Browse files
committed
Merge branch 'master' into no-scanning-restarts-on-duplicate-detection-devices
2 parents c1dacc9 + ef90455 commit 1e49cd6

File tree

7 files changed

+118
-58
lines changed

7 files changed

+118
-58
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Bug Fixes:
1111
- Deprecate misspelled methods `removeMonitoreNotifier` and
1212
`setRegionStatePeristenceEnabled` in favor of correctly spelled alternatives.
1313
(#461, Marco Salis)
14+
- Fix bug causing brief scan dropouts after starting a scan after a long period
15+
of inactivity (i.e. startup and background-foreground transitions) due to
16+
Android N scan limits (#489, Aaron Kromer)
17+
- Ensure thread safety for singleton creation of `BeaconManager`,
18+
`DetectionTracker`, `MonitoringStatus`, and `Stats`. (#494, Aaron Kromer)
1419

1520

1621
### 2.9.2 / 2016-11-22

src/main/java/org/altbeacon/beacon/BeaconManager.java

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
public class BeaconManager {
109109
private static final String TAG = "BeaconManager";
110110
private Context mContext;
111-
protected static BeaconManager client = null;
111+
protected static volatile BeaconManager client = null;
112112
private final ConcurrentMap<BeaconConsumer, ConsumerInfo> consumers = new ConcurrentHashMap<BeaconConsumer,ConsumerInfo>();
113113
private Messenger serviceMessenger = null;
114114
protected final Set<RangeNotifier> rangeNotifiers = new CopyOnWriteArraySet<>();
@@ -123,6 +123,11 @@ public class BeaconManager {
123123
private static boolean sAndroidLScanningDisabled = false;
124124
private static boolean sManifestCheckingDisabled = false;
125125

126+
/**
127+
* Private lock object for singleton initialization protecting against denial-of-service attack.
128+
*/
129+
private static final Object SINGLETON_LOCK = new Object();
130+
126131
/**
127132
* Set to true if you want to show library debugging.
128133
*
@@ -239,11 +244,29 @@ public static long getRegionExitPeriod(){
239244
* or non-Service class, you can attach it to another singleton or a subclass of the Android Application class.
240245
*/
241246
public static BeaconManager getInstanceForApplication(Context context) {
242-
if (client == null) {
243-
LogManager.d(TAG, "BeaconManager instance creation");
244-
client = new BeaconManager(context);
247+
/*
248+
* Follow double check pattern from Effective Java v2 Item 71.
249+
*
250+
* Bloch recommends using the local variable for this for performance reasons:
251+
*
252+
* > What this variable does is ensure that `field` is read only once in the common case
253+
* > where it's already initialized. While not strictly necessary, this may improve
254+
* > performance and is more elegant by the standards applied to low-level concurrent
255+
* > programming. On my machine, [this] is about 25 percent faster than the obvious
256+
* > version without a local variable.
257+
*
258+
* Joshua Bloch. Effective Java, Second Edition. Addison-Wesley, 2008. pages 283-284
259+
*/
260+
BeaconManager instance = client;
261+
if (instance == null) {
262+
synchronized (SINGLETON_LOCK) {
263+
instance = client;
264+
if (instance == null) {
265+
client = instance = new BeaconManager(context);
266+
}
267+
}
245268
}
246-
return client;
269+
return instance;
247270
}
248271

249272
protected BeaconManager(Context context) {
@@ -271,17 +294,10 @@ public List<BeaconParser> getBeaconParsers() {
271294
*/
272295
@TargetApi(18)
273296
public boolean checkAvailability() throws BleNotAvailableException {
274-
if (android.os.Build.VERSION.SDK_INT < 18) {
297+
if (!isBleAvailable()) {
275298
throw new BleNotAvailableException("Bluetooth LE not supported by this device");
276299
}
277-
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
278-
throw new BleNotAvailableException("Bluetooth LE not supported by this device");
279-
} else {
280-
if (((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled()) {
281-
return true;
282-
}
283-
}
284-
return false;
300+
return ((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter().isEnabled();
285301
}
286302

287303
/**
@@ -292,8 +308,12 @@ public boolean checkAvailability() throws BleNotAvailableException {
292308
* @param consumer the <code>Activity</code> or <code>Service</code> that will receive the callback when the service is ready.
293309
*/
294310
public void bind(BeaconConsumer consumer) {
295-
if (android.os.Build.VERSION.SDK_INT < 18) {
296-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
311+
if (!isBleAvailable()) {
312+
LogManager.w(TAG, "Method invocation will be ignored.");
313+
return;
314+
}
315+
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
316+
LogManager.w(TAG, "This device does not support bluetooth LE. Will not start beacon scanning.");
297317
return;
298318
}
299319
synchronized (consumers) {
@@ -318,8 +338,8 @@ public void bind(BeaconConsumer consumer) {
318338
* @param consumer the <code>Activity</code> or <code>Service</code> that no longer needs to use the service.
319339
*/
320340
public void unbind(BeaconConsumer consumer) {
321-
if (android.os.Build.VERSION.SDK_INT < 18) {
322-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
341+
if (!isBleAvailable()) {
342+
LogManager.w(TAG, "Method invocation will be ignored.");
323343
return;
324344
}
325345
synchronized (consumers) {
@@ -391,8 +411,9 @@ public boolean isAnyConsumerBound() {
391411
* @see #setBackgroundBetweenScanPeriod(long p)
392412
*/
393413
public void setBackgroundMode(boolean backgroundMode) {
394-
if (android.os.Build.VERSION.SDK_INT < 18) {
395-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
414+
if (!isBleAvailable()) {
415+
LogManager.w(TAG, "Method invocation will be ignored.");
416+
return;
396417
}
397418
mBackgroundModeUninitialized = false;
398419
if (backgroundMode != mBackgroundMode) {
@@ -608,8 +629,8 @@ public void requestStateForRegion(Region region) {
608629
*/
609630
@TargetApi(18)
610631
public void startRangingBeaconsInRegion(Region region) throws RemoteException {
611-
if (android.os.Build.VERSION.SDK_INT < 18) {
612-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
632+
if (!isBleAvailable()) {
633+
LogManager.w(TAG, "Method invocation will be ignored.");
613634
return;
614635
}
615636
if (serviceMessenger == null) {
@@ -636,8 +657,8 @@ public void startRangingBeaconsInRegion(Region region) throws RemoteException {
636657
*/
637658
@TargetApi(18)
638659
public void stopRangingBeaconsInRegion(Region region) throws RemoteException {
639-
if (android.os.Build.VERSION.SDK_INT < 18) {
640-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
660+
if (!isBleAvailable()) {
661+
LogManager.w(TAG, "Method invocation will be ignored.");
641662
return;
642663
}
643664
if (serviceMessenger == null) {
@@ -671,8 +692,8 @@ public void stopRangingBeaconsInRegion(Region region) throws RemoteException {
671692
*/
672693
@TargetApi(18)
673694
public void startMonitoringBeaconsInRegion(Region region) throws RemoteException {
674-
if (android.os.Build.VERSION.SDK_INT < 18) {
675-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
695+
if (!isBleAvailable()) {
696+
LogManager.w(TAG, "Method invocation will be ignored.");
676697
return;
677698
}
678699
if (serviceMessenger == null) {
@@ -699,8 +720,8 @@ public void startMonitoringBeaconsInRegion(Region region) throws RemoteException
699720
*/
700721
@TargetApi(18)
701722
public void stopMonitoringBeaconsInRegion(Region region) throws RemoteException {
702-
if (android.os.Build.VERSION.SDK_INT < 18) {
703-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
723+
if (!isBleAvailable()) {
724+
LogManager.w(TAG, "Method invocation will be ignored.");
704725
return;
705726
}
706727
if (serviceMessenger == null) {
@@ -721,8 +742,8 @@ public void stopMonitoringBeaconsInRegion(Region region) throws RemoteException
721742
*/
722743
@TargetApi(18)
723744
public void updateScanPeriods() throws RemoteException {
724-
if (android.os.Build.VERSION.SDK_INT < 18) {
725-
LogManager.w(TAG, "Not supported prior to API 18. Method invocation will be ignored");
745+
if (!isBleAvailable()) {
746+
LogManager.w(TAG, "Method invocation will be ignored.");
726747
return;
727748
}
728749
if (serviceMessenger == null) {
@@ -895,6 +916,18 @@ public void setNonBeaconLeScanCallback(NonBeaconLeScanCallback callback) {
895916
mNonBeaconLeScanCallback = callback;
896917
}
897918

919+
private boolean isBleAvailable() {
920+
boolean available = false;
921+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
922+
LogManager.w(TAG, "Bluetooth LE not supported prior to API 18.");
923+
} else if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
924+
LogManager.w(TAG, "This device does not support bluetooth LE.");
925+
} else {
926+
available = true;
927+
}
928+
return available;
929+
}
930+
898931
private long getScanPeriod() {
899932
if (mBackgroundMode) {
900933
return backgroundScanPeriod;

src/main/java/org/altbeacon/beacon/powersave/BackgroundPowerSaver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public BackgroundPowerSaver(Context context) {
4444
LogManager.w(TAG, "BackgroundPowerSaver requires API 18 or higher.");
4545
return;
4646
}
47-
((Application)context.getApplicationContext()).registerActivityLifecycleCallbacks(this);
4847
beaconManager = BeaconManager.getInstanceForApplication(context);
48+
((Application)context.getApplicationContext()).registerActivityLifecycleCallbacks(this);
4949
}
5050

5151
@Override

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
* Created by dyoung on 1/10/15.
77
*/
88
public class DetectionTracker {
9-
private static DetectionTracker sDetectionTracker = null;
9+
private static final DetectionTracker INSTANCE = new DetectionTracker();
10+
1011
private long mLastDetectionTime = 0l;
1112
private DetectionTracker() {
1213

1314
}
14-
public static synchronized DetectionTracker getInstance() {
15-
if (sDetectionTracker == null) {
16-
sDetectionTracker = new DetectionTracker();
17-
}
18-
return sDetectionTracker;
15+
public static DetectionTracker getInstance() {
16+
return INSTANCE;
1917
}
2018
public long getLastDetectionTime() {
2119
return mLastDetectionTime;

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import static android.content.Context.MODE_PRIVATE;
2424

2525
public class MonitoringStatus {
26-
private static MonitoringStatus sInstance;
26+
private static volatile MonitoringStatus sInstance;
2727
private static final int MAX_REGIONS_FOR_STATUS_PRESERVATION = 50;
2828
private static final int MAX_STATUS_PRESERVATION_FILE_AGE_TO_RESTORE_SECS = 60 * 15;
2929
private static final String TAG = MonitoringStatus.class.getSimpleName();
@@ -35,15 +35,35 @@ public class MonitoringStatus {
3535

3636
private boolean mStatePreservationIsOn = true;
3737

38+
/**
39+
* Private lock object for singleton initialization protecting against denial-of-service attack.
40+
*/
41+
private static final Object SINGLETON_LOCK = new Object();
42+
3843
public static MonitoringStatus getInstanceForApplication(Context context) {
39-
if (sInstance == null) {
40-
synchronized (MonitoringStatus.class) {
41-
if (sInstance == null) {
42-
sInstance = new MonitoringStatus(context.getApplicationContext());
44+
/*
45+
* Follow double check pattern from Effective Java v2 Item 71.
46+
*
47+
* Bloch recommends using the local variable for this for performance reasons:
48+
*
49+
* > What this variable does is ensure that `field` is read only once in the common case
50+
* > where it's already initialized. While not strictly necessary, this may improve
51+
* > performance and is more elegant by the standards applied to low-level concurrent
52+
* > programming. On my machine, [this] is about 25 percent faster than the obvious
53+
* > version without a local variable.
54+
*
55+
* Joshua Bloch. Effective Java, Second Edition. Addison-Wesley, 2008. pages 283-284
56+
*/
57+
MonitoringStatus instance = sInstance;
58+
if (instance == null) {
59+
synchronized (SINGLETON_LOCK) {
60+
instance = sInstance;
61+
if (instance == null) {
62+
sInstance = instance = new MonitoringStatus(context.getApplicationContext());
4363
}
4464
}
4565
}
46-
return sInstance;
66+
return instance;
4767
}
4868

4969
public MonitoringStatus(Context context) {

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
* Created by dyoung on 10/16/14.
1313
*/
1414
public class Stats {
15+
private static final Stats INSTANCE = new Stats();
1516
private static final String TAG = "Stats";
17+
18+
/**
19+
* Synchronize all usage as this is not a thread safe class.
20+
*/
1621
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS");
1722

1823
private ArrayList<Sample> mSamples;
@@ -21,14 +26,11 @@ public class Stats {
2126
private boolean mEnableHistoricalLogging;
2227
private boolean mEnabled;
2328
private Sample mSample;
24-
private static Stats mInstance;
2529

2630
public static Stats getInstance() {
27-
if(mInstance == null) {
28-
mInstance = new Stats();
29-
}
30-
return mInstance;
31+
return INSTANCE;
3132
}
33+
3234
private Stats() {
3335
mSampleIntervalMillis = 0l;
3436
clearSamples();
@@ -105,7 +107,13 @@ private void logSample(Sample sample, boolean showHeader) {
105107
}
106108

107109
private String formattedDate(Date d) {
108-
return d == null ? "" : SIMPLE_DATE_FORMAT.format(d);
110+
String formattedDate = "";
111+
if (d != null) {
112+
synchronized (SIMPLE_DATE_FORMAT) {
113+
formattedDate = SIMPLE_DATE_FORMAT.format(d);
114+
}
115+
}
116+
return formattedDate;
109117
}
110118

111119
private void logSamples() {

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public abstract class CycledLeScanner {
3131
private long mLastScanCycleEndTime = 0l;
3232
protected long mNextScanCycleStartTime = 0l;
3333
private long mScanCycleStopTime = 0l;
34-
private long mLastScanStopTime = 0l;
3534

3635
private boolean mScanning;
3736
protected boolean mScanningPaused;
@@ -158,10 +157,8 @@ public void stop() {
158157
mScanningEnabled = false;
159158
if (mScanCyclerStarted) {
160159
scanLeDevice(false);
161-
}
162-
if (mBluetoothAdapter != null) {
163-
stopScan();
164-
mLastScanCycleEndTime = SystemClock.elapsedRealtime();
160+
} else {
161+
LogManager.d(TAG, "scanning already stopped");
165162
}
166163
}
167164

@@ -289,23 +286,22 @@ private void finishScanCycle() {
289286
// packets in the same cycle, we will not restart scanning and just keep it
290287
// going.
291288
if (!getDistinctPacketsDetectedPerScan()) {
292-
long now = System.currentTimeMillis();
289+
long now = SystemClock.elapsedRealtime();
293290
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
294291
mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS &&
295-
now-mLastScanStopTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) {
292+
now-mLastScanCycleStartTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) {
296293
// As of Android N, only 5 scans may be started in a 30 second period (6
297294
// seconds per cycle) otherwise they are blocked. So we check here to see
298295
// if the scan period is 6 seconds or less, and if we last stopped scanning
299296
// fewer than 6 seconds ag and if so, we simply do not stop scanning
300297
LogManager.d(TAG, "Not stopping scan because this is Android N and we" +
301298
" keep scanning for a minimum of 6 seconds at a time. "+
302-
"We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanStopTime))+" millisconds.");
299+
"We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanCycleStartTime))+" millisconds.");
303300
}
304301
else {
305302
try {
306303
LogManager.d(TAG, "stopping bluetooth le scan");
307304
finishScan();
308-
mLastScanStopTime = now;
309305
} catch (Exception e) {
310306
LogManager.w(e, TAG, "Internal Android exception scanning for beacons");
311307
}

0 commit comments

Comments
 (0)