-
Notifications
You must be signed in to change notification settings - Fork 836
/
CycledLeScannerForLollipop.java
427 lines (403 loc) · 21 KB
/
CycledLeScannerForLollipop.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package org.altbeacon.beacon.service.scanner;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.SystemClock;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.service.DetectionTracker;
import org.altbeacon.bluetooth.BluetoothCrashResolver;
import org.altbeacon.bluetooth.BluetoothMedic;
import java.util.ArrayList;
import java.util.List;
@TargetApi(21)
public class CycledLeScannerForLollipop extends CycledLeScanner {
private static final String TAG = "CycledLeScannerForLollipop";
private static final long BACKGROUND_L_SCAN_DETECTION_PERIOD_MILLIS = 10000l;
private BluetoothLeScanner mScanner;
private ScanCallback leScanCallback;
private long mBackgroundLScanStartTime = 0l;
private long mBackgroundLScanFirstDetectionTime = 0;
private boolean mMainScanCycleActive = false;
private final BeaconManager mBeaconManager;
private final PowerManager mPowerManager;
public CycledLeScannerForLollipop(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) {
super(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver);
mBeaconManager = BeaconManager.getInstanceForApplication(mContext);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
}
@Override
protected void stopScan() {
postStopLeScan();
}
/*
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, make 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.
*/
protected boolean deferScanIfNeeded() {
// This method is called to see if it is time to start a scan
long millisecondsUntilStart = mNextScanCycleStartTime - SystemClock.elapsedRealtime();
final boolean deferScan = millisecondsUntilStart > 0;
final boolean scanActiveBefore = mMainScanCycleActive;
mMainScanCycleActive = !deferScan;
if (deferScan) {
long secsSinceLastDetection = SystemClock.elapsedRealtime() -
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 (scanActiveBefore) {
if (secsSinceLastDetection > BACKGROUND_L_SCAN_DETECTION_PERIOD_MILLIS) {
mBackgroundLScanStartTime = SystemClock.elapsedRealtime();
mBackgroundLScanFirstDetectionTime = 0l;
LogManager.d(TAG, "This is Android L. Preparing to do 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
// Only scan between cycles if the between can cycle time > 6 seconds. A shorter low
// power scan is unlikely to be useful, and might trigger a "scanning too frequently"
// error on Android N.
if (mBetweenScanPeriod > 6000l) {
startScan();
}
else {
LogManager.d(TAG, "Suppressing scan between cycles because the between scan cycle is too short.");
}
} else {
// TODO: Consider starting a scan with delivery based on the filters *NOT* being seen
// This API is now available in Android M
LogManager.d(TAG, "This is Android L, but we last saw a beacon only %s "
+ "ago, so we will not keep scanning in background.",
secsSinceLastDetection);
}
}
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 (SystemClock.elapsedRealtime() - 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
LogManager.d(TAG, "We've been detecting for a bit. Stopping Android L background scanning");
stopScan();
mBackgroundLScanStartTime = 0l;
}
else {
// report the results up the chain
LogManager.d(TAG, "Delivering Android L background scanning results");
mCycledLeScanCallback.onCycleEnd();
}
}
}
LogManager.d(TAG, "Waiting to start full Bluetooth scan for another %s milliseconds",
millisecondsUntilStart);
// 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 (scanActiveBefore && mBackgroundFlag) {
setWakeUpAlarm();
}
mHandler.postDelayed(new Runnable() {
@MainThread
@Override
public void run() {
scanLeDevice(true);
}
}, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart);
} else {
if (mBackgroundLScanStartTime > 0l) {
stopScan();
mBackgroundLScanStartTime = 0;
}
}
return deferScan;
}
@Override
protected void startScan() {
if (!isBluetoothOn() && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
LogManager.d(TAG, "Not starting scan because bluetooth is off");
return;
}
List<ScanFilter> filters = new ArrayList<ScanFilter>();
ScanSettings settings = null;
if (!mMainScanCycleActive) {
LogManager.d(TAG, "starting filtered scan in SCAN_MODE_LOW_POWER");
settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build();
filters = new ScanFilterUtils().createScanFiltersForBeaconParsers(
mBeaconManager.getBeaconParsers());
} else {
LogManager.d(TAG, "starting a scan in SCAN_MODE_LOW_LATENCY");
settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)).build();
// We create scan filters that match so that we can detect
// beacons in foreground mode even if the screen is off. This is a necessary workaround
// for a change in Android 8.1 that blocks scan results when the screen is off unless
// there is a scan filter associated with the scan. Prior to 8.1, filters could just be
// left null.
// We only add these filters on 8.1+ devices, because adding scan filters has been reported
// to cause scan failures on some Samsung devices with Android 5.x
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
if ((Build.MANUFACTURER.equalsIgnoreCase("samsung") ||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) &&
!mPowerManager.isInteractive()) {
// On the Samsung 8.1 and Android 14.0, scans are blocked with screen off when the
// scan filter is empty (wildcard). We do a more detailed filter on such devices
// only because it might block detections of AltBeacon packets with non-standard
// manufacturer codes. See #769 for details.
LogManager.d(TAG, "Using a non-empty scan filter since this is 14.0 or Samsung 8.1+");
filters = new ScanFilterUtils().createScanFiltersForBeaconParsers(
mBeaconManager.getBeaconParsers());
}
else {
if (Build.MANUFACTURER.equalsIgnoreCase("samsung")) {
LogManager.d(TAG, "Using a wildcard scan filter because the screen is on. We will switch to a non-empty filter if the screen goes off");
// as soon as the screen goes off we will need to start a different scan
// that has scan filters
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
mContext.getApplicationContext().registerReceiver(mScreenOffReceiver, filter);
LogManager.d(TAG, "registering ScreenOffReceiver "+mScreenOffReceiver);
}
// The wildcard filter matches everything.
filters = new ScanFilterUtils().createWildcardScanFilters();
}
}
else {
LogManager.d(TAG, "Using no scan filter since this is pre-8.1");
}
}
if (settings != null) {
postStartLeScan(filters, settings);
}
}
@MainThread
public void stop() {
super.stop();
LogManager.d(TAG, "unregistering ScreenOffReceiver as we stop the cycled scanner");
// Catch the exception in case it has not been registered
try { mContext.getApplicationContext().unregisterReceiver(mScreenOffReceiver); } catch (IllegalArgumentException e) {}
}
@Override
protected void finishScan() {
LogManager.d(TAG, "Stopping scan");
stopScan();
mScanningPaused = true;
}
private void postStartLeScan(final List<ScanFilter> filters, final ScanSettings settings) {
final BluetoothLeScanner scanner = getScanner();
if (scanner == null) {
return;
}
final ScanCallback scanCallback = getNewLeScanCallback();
mScanHandler.removeCallbacksAndMessages(null);
mScanHandler.post(new Runnable() {
@WorkerThread
@Override
public void run() {
try {
scanner.startScan(filters, settings, scanCallback);
} catch (IllegalStateException e) {
LogManager.w(TAG, "Cannot start scan. Bluetooth may be turned off.");
} catch (NullPointerException npe) {
// Necessary because of https://code.google.com/p/android/issues/detail?id=160503
LogManager.e(npe, TAG, "Cannot start scan. Unexpected NPE.");
} catch (SecurityException e) {
// Thrown by Samsung Knox devices if bluetooth access denied for an app
LogManager.e(TAG, "Cannot start scan. Security Exception: "+e.getMessage(), e);
}
}
});
}
private void postStopLeScan() {
if (!isBluetoothOn() && Build.VERSION.SDK_INT < Build.VERSION_CODES.P){
LogManager.d(TAG, "Not stopping scan because bluetooth is off");
return;
}
final BluetoothLeScanner scanner = getScanner();
if (scanner == null) {
return;
}
final ScanCallback scanCallback = getNewLeScanCallback();
mScanHandler.removeCallbacksAndMessages(null);
mScanHandler.post(new Runnable() {
@WorkerThread
@Override
public void run() {
try {
LogManager.d(TAG, "Stopping LE scan on scan handler");
scanner.stopScan(scanCallback);
} catch (IllegalStateException e) {
LogManager.w(TAG, "Cannot stop scan. Bluetooth may be turned off.");
} catch (NullPointerException npe) {
// Necessary because of https://code.google.com/p/android/issues/detail?id=160503
LogManager.e(npe, TAG, "Cannot stop scan. Unexpected NPE.");
} catch (SecurityException e) {
// Thrown by Samsung Knox devices if bluetooth access denied for an app
LogManager.e(TAG, "Cannot stop scan. Security Exception", e);
}
}
});
}
private boolean isBluetoothOn() {
try {
BluetoothAdapter bluetoothAdapter = getBluetoothAdapter();
if (bluetoothAdapter != null) {
return (bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON);
}
LogManager.w(TAG, "Cannot get bluetooth adapter");
}
catch (SecurityException e) {
LogManager.w(TAG, "SecurityException checking if bluetooth is on", e);
}
return false;
}
private BluetoothLeScanner getScanner() {
try {
if (mScanner == null) {
LogManager.d(TAG, "Making new Android L scanner");
BluetoothAdapter bluetoothAdapter = getBluetoothAdapter();
if (bluetoothAdapter != null) {
mScanner = getBluetoothAdapter().getBluetoothLeScanner();
}
if (mScanner == null) {
LogManager.w(TAG, "Failed to make new Android L scanner");
}
}
}
catch (SecurityException e) {
LogManager.w(TAG, "SecurityException making new Android L scanner", e);
}
return mScanner;
}
private ScanCallback getNewLeScanCallback() {
if (leScanCallback == null) {
leScanCallback = new ScanCallback() {
@MainThread
@Override
public void onScanResult(int callbackType, ScanResult scanResult) {
if (LogManager.isVerboseLoggingEnabled()) {
LogManager.d(TAG, "got record");
List<ParcelUuid> uuids = scanResult.getScanRecord().getServiceUuids();
if (uuids != null) {
for (ParcelUuid uuid : uuids) {
LogManager.d(TAG, "with service uuid: "+uuid);
}
}
}
mCycledLeScanCallback.onLeScan(scanResult.getDevice(),
scanResult.getRssi(), scanResult.getScanRecord().getBytes(),
System.currentTimeMillis() - SystemClock.elapsedRealtime() + scanResult.getTimestampNanos() / 1000000);
if (mBackgroundLScanStartTime > 0) {
LogManager.d(TAG, "got a filtered scan result in the background.");
}
}
@MainThread
@Override
public void onBatchScanResults(List<ScanResult> results) {
LogManager.d(TAG, "got batch records");
for (ScanResult scanResult : results) {
mCycledLeScanCallback.onLeScan(scanResult.getDevice(),
scanResult.getRssi(), scanResult.getScanRecord().getBytes(),
System.currentTimeMillis() - SystemClock.elapsedRealtime() + scanResult.getTimestampNanos() / 1000000);
}
if (mBackgroundLScanStartTime > 0) {
LogManager.d(TAG, "got a filtered batch scan result in the background.");
}
}
@MainThread
@Override
public void onScanFailed(int errorCode) {
BluetoothMedic.getInstance().processMedicAction("onScanFailed", errorCode);
switch (errorCode) {
case SCAN_FAILED_ALREADY_STARTED:
LogManager.e(
TAG,
"Scan failed: a BLE scan with the same settings is already started by the app"
);
break;
case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
LogManager.e(
TAG,
"Scan failed: app cannot be registered"
);
break;
case SCAN_FAILED_FEATURE_UNSUPPORTED:
LogManager.e(
TAG,
"Scan failed: power optimized scan feature is not supported"
);
break;
case SCAN_FAILED_INTERNAL_ERROR:
LogManager.e(
TAG,
"Scan failed: internal error"
);
break;
default:
LogManager.e(
TAG,
"Scan failed with unknown error (errorCode=" + errorCode + ")"
);
break;
}
}
};
}
return leScanCallback;
}
private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!mMainScanCycleActive) {
LogManager.d(TAG, "Screen has gone off while outside the main scan cycle. We will do nothing.");
}
else {
LogManager.d(TAG, "Screen has gone off while using a wildcard scan filter. Restarting scanner with non-empty filters.");
stopScan();
startScan();
}
}
};
}