-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathBluetoothCrashResolver.java
More file actions
476 lines (432 loc) · 18.8 KB
/
BluetoothCrashResolver.java
File metadata and controls
476 lines (432 loc) · 18.8 KB
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
package com.radiusnetworks.bluetooth;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
*
* This class provides relief for Android Bug 67272. This bug in the Bluedroid stack causes crashes
* in Android's BluetoothService when scanning for BLE devices encounters a large number of unique
* devices. It is rare for most users but can be problematic for those with apps scanning for
* Bluetooth LE devices in the background (e.g. iBeacon-enabled apps), especially when these users
* are around Bluetooth LE devices that randomize their mac address like Gimbal beacons.
*
* This class can both recover from crashes and prevent crashes from happening in the first place
*
* More details on the bug can be found at the following URLs:
*
* https://code.google.com/p/android/issues/detail?id=67272
* https://github.com/RadiusNetworks/android-ibeacon-service/issues/16
*
* Version 1.0
*
* Created by dyoung on 3/24/14.
*/
@TargetApi(5)
public class BluetoothCrashResolver {
private static final String TAG = "BluetoothCrashResolver";
private static final boolean PREEMPTIVE_ACTION_ENABLED = true;
private boolean debugEnabled = false;
/**
* This is not the same file that bluedroid uses. This is just to maintain state of this module
*/
private static final String DISTINCT_BLUETOOTH_ADDRESSES_FILE = "BluetoothCrashResolverState.txt";
private boolean recoveryInProgress = false;
private boolean discoveryStartConfirmed = false;
private long lastBluetoothOffTime = 0l;
private long lastBluetoothTurningOnTime = 0l;
private long lastBluetoothCrashDetectionTime = 0l;
private int detectedCrashCount = 0;
private int recoveryAttemptCount = 0;
private boolean lastRecoverySucceeded = false;
private long lastStateSaveTime = 0l;
private static final long MIN_TIME_BETWEEN_STATE_SAVES_MILLIS = 60000l;
private Context context = null;
private UpdateNotifier updateNotifier;
private Set<String> distinctBluetoothAddresses = new HashSet<String>();
private DiscoveryCanceller discoveryCanceller = new DiscoveryCanceller();
/**
// It is very likely a crash if Bluetooth turns off and comes
// back on in an extremely short interval. Testing on a Nexus 4 shows
// that when the BluetoothService crashes, the time between the STATE_OFF
// and the STATE_TURNING_ON ranges from 0ms-684ms
// Out of 3614 samples:
// 99.4% (3593) < 600 ms
// 84.7% (3060) < 500 ms
// So we will assume any power off sequence of < 600ms to be a crash
//
// While it is possible to manually turn bluetooth off then back on in
// about 600ms, but it is pretty hard to do.
//
*/
private static final long SUSPICIOUSLY_SHORT_BLUETOOTH_OFF_INTERVAL_MILLIS = 600l;
/**
* The Bluedroid stack can only track only 1990 unique Bluetooth mac addresses without crashing
*/
private static final int BLUEDROID_MAX_BLUETOOTH_MAC_COUNT = 1990;
/**
* The discovery process will pare back the mac address list to 256, but more may
* be found in the time we let the discovery process run, depending hon how many BLE
* devices are around.
*/
private static final int BLUEDROID_POST_DISCOVERY_ESTIMATED_BLUETOOTH_MAC_COUNT = 400;
/**
* It takes a little over 2 seconds after discovery is started before the pared-down mac file
* is written to persistent storage. We let discovery run for a few more seconds just to be
* sure.
*/
private static final int TIME_TO_LET_DISCOVERY_RUN_MILLIS = 5000; /* if 0, it means forever */
/**
* Constructor should be called only once per long-running process that does Bluetooth LE
* scanning. Must call start() to make it do anything.
*
* @param context the Activity or Service that is doing the Bluetooth scanning
*/
public BluetoothCrashResolver(Context context) {
this.context = context.getApplicationContext();
if (isDebugEnabled()) Log.d(TAG, "constructed");
loadState();
}
/**
* Starts looking for crashes of the Bluetooth LE system and taking proactive steps to stop
* crashes from happening. Proactive steps require calls to notifyScannedDevice(Device device)
* so that crashes can be predicted ahead of time.
*/
public void start() {
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
context.registerReceiver(receiver, filter);
if (isDebugEnabled()) Log.d(TAG, "started listening for BluetoothAdapter events");
}
/**
* Stops looking for crashes. Does not need to be called in normal operations, but may be
* useful for testing.
*/
public void stop() {
context.unregisterReceiver(receiver);
if (isDebugEnabled()) Log.d(TAG, "stopped listening for BluetoothAdapter events");
saveState();
}
/**
* Enable debug logging. By default no debug lines are logged.
*/
public void enableDebug() {
debugEnabled = true;
}
/**
* Disable debug logging
*/
public void disableDebug() {
debugEnabled = false;
}
/**
* Call this method from your BluetoothAdapter.LeScanCallback method.
* Doing so is optional, but if you do, this class will be able to count the number of
* disctinct bluetooth devices scanned, and prevent crashes before they happen.
*
* This works very well if the app containing this class is the only one running bluetooth
* LE scans on the device, or it is constantly doing scans (e.g. is in the foreground for
* extended periods of time.)
*
* This will not work well if the application using this class is only scanning periodically
* (e.g. when in the background to save battery) and another application is also scanning on
* the same device, because this class will only get the counts from this application.
*
* Future augmentation of this class may improve this by somehow centralizing the list of
* unique scanned devices.
*
* @param device
*/
@TargetApi(18)
public void notifyScannedDevice(BluetoothDevice device, BluetoothAdapter.LeScanCallback scanner) {
int oldSize = 0, newSize = 0;
if (isDebugEnabled()) oldSize = distinctBluetoothAddresses.size();
distinctBluetoothAddresses.add(device.getAddress());
if (isDebugEnabled()) {
newSize = distinctBluetoothAddresses.size();
if (oldSize != newSize && newSize % 100 == 0) {
if (isDebugEnabled()) Log.d(TAG, "Distinct bluetooth devices seen: "+distinctBluetoothAddresses.size());
}
}
if (distinctBluetoothAddresses.size() > getCrashRiskDeviceCount()) {
if (PREEMPTIVE_ACTION_ENABLED && !recoveryInProgress) {
Log.w(TAG, "Large number of bluetooth devices detected: "+distinctBluetoothAddresses.size()+" Proactively attempting to clear out address list to prevent a crash");
Log.w(TAG, "Stopping LE Scan");
BluetoothAdapter.getDefaultAdapter().stopLeScan(scanner);
startRecovery();
processStateChange();
}
}
}
public void crashDetected() {
if (android.os.Build.VERSION.SDK_INT < 18) {
if (isDebugEnabled()) Log.d(TAG, "Ignoring crashes before SDK 18, because BLE is unsupported.");
return;
}
Log.w(TAG, "BluetoothService crash detected");
if (distinctBluetoothAddresses.size() > 0) {
if (isDebugEnabled()) Log.d(TAG, "Distinct bluetooth devices seen at crash: "+distinctBluetoothAddresses.size());
}
long nowTimestamp = new Date().getTime();
lastBluetoothCrashDetectionTime = nowTimestamp;
detectedCrashCount++;
if (recoveryInProgress) {
if (isDebugEnabled()) Log.d(TAG, "Ignoring bluetooth crash because recovery is already in progress.");
}
else {
startRecovery();
}
processStateChange();
}
public long getLastBluetoothCrashDetectionTime() {
return lastBluetoothCrashDetectionTime;
}
public int getDetectedCrashCount() {
return detectedCrashCount;
}
public int getRecoveryAttemptCount() {
return recoveryAttemptCount;
}
public boolean isLastRecoverySucceeded() {
return lastRecoverySucceeded;
}
public boolean isRecoveryInProgress() { return recoveryInProgress; }
public interface UpdateNotifier {
public void dataUpdated();
}
public void setUpdateNotifier(UpdateNotifier updateNotifier) {
this.updateNotifier = updateNotifier;
}
/**
Used to force a recovery operation
*/
public void forceFlush() {
startRecovery();
processStateChange();
}
private boolean isDebugEnabled() {
return debugEnabled;
}
private int getCrashRiskDeviceCount() {
// 1990 distinct devices tracked by Bluedroid will cause a crash. But we don't know how many
// devices bluedroid is tracking, we only know how many we have seen, which will be smaller
// than the number tracked by bluedroid because the number we track does not include its
// initial state. We therefore assume that there are some devices being tracked by bluedroid
// after a recovery operation or on startup
return BLUEDROID_MAX_BLUETOOTH_MAC_COUNT-BLUEDROID_POST_DISCOVERY_ESTIMATED_BLUETOOTH_MAC_COUNT;
}
private void processStateChange() {
if (updateNotifier != null) {
updateNotifier.dataUpdated();
}
if (new Date().getTime() - lastStateSaveTime > MIN_TIME_BETWEEN_STATE_SAVES_MILLIS) {
saveState();
}
}
@TargetApi(17)
private void startRecovery() {
// The discovery operation will start by clearing out the bluetooth mac list to only the 256
// most recently seen BLE mac addresses.
recoveryAttemptCount++;
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (isDebugEnabled()) Log.d(TAG, "about to check if discovery is active");
if (!adapter.isDiscovering()) {
Log.w(TAG, "Recovery attempt started");
recoveryInProgress = true;
discoveryStartConfirmed = false;
if (isDebugEnabled()) Log.d(TAG, "about to command discovery");
if (!adapter.startDiscovery()) {
Log.w(TAG, "Can't start discovery. Is bluetooth turned on?");
}
if (isDebugEnabled()) Log.d(TAG, "startDiscovery commanded. isDiscovering()="+adapter.isDiscovering());
// We don't actually need to do a discovery -- we just need to kick one off so the
// mac list will be pared back to 256. Because discovery is an expensive operation in
// terms of battery, we will cancel it.
if (TIME_TO_LET_DISCOVERY_RUN_MILLIS > 0 ) {
if (isDebugEnabled()) Log.d(TAG, "We will be cancelling this discovery in "+TIME_TO_LET_DISCOVERY_RUN_MILLIS+" milliseconds.");
discoveryCanceller.doInBackground();
}
else {
Log.d(TAG, "We will let this discovery run its course.");
}
}
else {
Log.w(TAG, "Already discovering. Recovery attempt abandoned.");
}
}
private void finishRecovery() {
Log.w(TAG, "Recovery attempt finished");
distinctBluetoothAddresses.clear();
recoveryInProgress = false;
}
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
if (recoveryInProgress) {
if (isDebugEnabled()) Log.d(TAG, "Bluetooth discovery finished");
finishRecovery();
}
else {
if (isDebugEnabled()) Log.d(TAG, "Bluetooth discovery finished (external)");
}
}
if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_STARTED)) {
if (recoveryInProgress) {
discoveryStartConfirmed = true;
if (isDebugEnabled()) Log.d(TAG, "Bluetooth discovery started");
}
else {
if (isDebugEnabled()) Log.d(TAG, "Bluetooth discovery started (external)");
}
}
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.ERROR:
if (isDebugEnabled()) Log.d(TAG, "Bluetooth state is ERROR");
break;
case BluetoothAdapter.STATE_OFF:
if (isDebugEnabled()) Log.d(TAG, "Bluetooth state is OFF");
lastBluetoothOffTime = new Date().getTime();
break;
case BluetoothAdapter.STATE_TURNING_OFF:
break;
case BluetoothAdapter.STATE_ON:
if (isDebugEnabled()) Log.d(TAG, "Bluetooth state is ON");
if (isDebugEnabled()) Log.d(TAG, "Bluetooth was turned off for "+(lastBluetoothTurningOnTime - lastBluetoothOffTime)+" milliseconds");
if (lastBluetoothTurningOnTime - lastBluetoothOffTime < SUSPICIOUSLY_SHORT_BLUETOOTH_OFF_INTERVAL_MILLIS) {
crashDetected();
}
break;
case BluetoothAdapter.STATE_TURNING_ON:
lastBluetoothTurningOnTime = new Date().getTime();
if (isDebugEnabled()) Log.d(TAG, "Bluetooth state is TURNING_ON");
break;
}
}
}
};
private void saveState() {
FileOutputStream outputStream = null;
OutputStreamWriter writer = null;
lastStateSaveTime = new Date().getTime();
try {
outputStream = context.openFileOutput(DISTINCT_BLUETOOTH_ADDRESSES_FILE, Context.MODE_PRIVATE);
writer = new OutputStreamWriter(outputStream);
writer.write(lastBluetoothCrashDetectionTime+"\n");
writer.write(detectedCrashCount+"\n");
writer.write(recoveryAttemptCount+"\n");
writer.write(lastRecoverySucceeded ? "1\n" : "0\n");
synchronized (distinctBluetoothAddresses) {
for (String mac : distinctBluetoothAddresses) {
writer.write(mac);
writer.write("\n");
}
}
} catch (IOException e) {
Log.w(TAG, "Can't write macs to "+DISTINCT_BLUETOOTH_ADDRESSES_FILE);
}
finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e1) { }
}
}
if (isDebugEnabled()) Log.d(TAG, "Wrote "+distinctBluetoothAddresses.size()+" bluetooth addresses");
}
private void loadState() {
FileInputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = context.openFileInput(DISTINCT_BLUETOOTH_ADDRESSES_FILE);
reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
line = reader.readLine();
if (line != null) {
lastBluetoothCrashDetectionTime = Long.parseLong(line);
}
line = reader.readLine();
if (line != null) {
detectedCrashCount = Integer.parseInt(line);
}
line = reader.readLine();
if (line != null) {
recoveryAttemptCount = Integer.parseInt(line);
}
line = reader.readLine();
if (line != null) {
lastRecoverySucceeded = false;
if (line.equals("1")) {
lastRecoverySucceeded = true;
}
}
String mac;
while ((mac = reader.readLine()) != null) {
distinctBluetoothAddresses.add(mac);
}
} catch (IOException e) {
Log.w(TAG, "Can't read macs from "+DISTINCT_BLUETOOTH_ADDRESSES_FILE);
} catch (NumberFormatException e) {
Log.w(TAG, "Can't parse file "+DISTINCT_BLUETOOTH_ADDRESSES_FILE);
}
finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) { }
}
}
if (isDebugEnabled()) Log.d(TAG, "Read "+distinctBluetoothAddresses.size()+" bluetooth addresses");
}
private class DiscoveryCanceller extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
Thread.sleep(TIME_TO_LET_DISCOVERY_RUN_MILLIS);
if (!discoveryStartConfirmed) {
Log.w(TAG, "BluetoothAdapter.ACTION_DISCOVERY_STARTED never received. Recovery may fail.");
}
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter.isDiscovering()) {
if (isDebugEnabled()) Log.d(TAG, "Cancelling discovery");
adapter.cancelDiscovery();
}
else {
if (isDebugEnabled()) Log.d(TAG, "Discovery not running. Won't cancel it");
}
} catch (InterruptedException e) {
if (isDebugEnabled()) Log.d(TAG, "DiscoveryCanceller sleep interrupted.");
}
return null;
}
@Override
protected void onPostExecute(Void result) {
}
@Override
protected void onPreExecute() {
}
@Override
protected void onProgressUpdate(Void... values) {
}
}
}