Skip to content

Commit

Permalink
Merge pull request #896 from AltBeacon/fix-crash-on-job-stop
Browse files Browse the repository at this point in the history
Fix crash on job stop
  • Loading branch information
davidgyoung authored Jul 9, 2019
2 parents 57a5f1c + 2bb4773 commit b3129cf
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 200 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
### Development

- Prevent NPE in BluetoothMedic#runScanTest. (#893 Van Hoang Tran)
- Fix thread leak with 0 regions and settings applied, (#888, David G. Young)
- Prevent NPE when ScanState restore hangs or ScanJob thread is slow to start (#890, David G. Young)
- Prevent crash from IAE when ending scan cycle (#891, David G. Young)
- Prevent NPE in BluetoothMedic#runScanTest. (#893 Van Hoang Tran)

### 2.16.2 / 2019-05-29

Expand Down
268 changes: 159 additions & 109 deletions lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.Nullable;

import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconManager;
Expand Down Expand Up @@ -47,16 +48,20 @@ public class ScanJob extends JobService {
private static int sOverrideImmediateScanJobId = -1;
private static int sOverridePeriodicScanJobId = -1;

private ScanState mScanState;
@Nullable
private ScanState mScanState = null;
private Handler mStopHandler = new Handler();
@Nullable
private ScanHelper mScanHelper;
private boolean mInitialized = false;
private boolean mStopCalled = false;

@Override
public boolean onStartJob(final JobParameters jobParameters) {
// We start off on the main UI thread here.
// But the ScanState restore from storage sometimes hangs, so we start new thread here just
// to kick that off. This way if the restore hangs, we don't hang the UI thread.
LogManager.d(TAG, "ScanJob Lifecycle START: "+ScanJob.this);
new Thread(new Runnable() {
public void run() {
if (!initialzeScanHelper()) {
Expand All @@ -65,179 +70,224 @@ public void run() {
}
ScanJobScheduler.getInstance().ensureNotificationProcessorSetup(getApplicationContext());
if (jobParameters.getJobId() == getImmediateScanJobId(ScanJob.this)) {
LogManager.i(TAG, "Running immediate scan job: instance is "+this);
LogManager.i(TAG, "Running immediate scan job: instance is "+ScanJob.this);
}
else {
LogManager.i(TAG, "Running periodic scan job: instance is "+this);
LogManager.i(TAG, "Running periodic scan job: instance is "+ScanJob.this);
}

List<ScanResult> queuedScanResults = ScanJobScheduler.getInstance().dumpBackgroundScanResultQueue();
LogManager.d(TAG, "Processing %d queued scan resuilts", queuedScanResults.size());
for (ScanResult result : queuedScanResults) {
ScanRecord scanRecord = result.getScanRecord();
if (scanRecord != null) {
mScanHelper.processScanResult(result.getDevice(), result.getRssi(), scanRecord.getBytes());
if (mScanHelper != null) {
mScanHelper.processScanResult(result.getDevice(), result.getRssi(), scanRecord.getBytes());
}
}
}
LogManager.d(TAG, "Done processing queued scan resuilts");

boolean startedScan;
if (mInitialized) {
LogManager.d(TAG, "Scanning already started. Resetting for current parameters");
startedScan = restartScanning();
}
else {
startedScan = startScanning();
}
mStopHandler.removeCallbacksAndMessages(null);
// This syncronized block is around the scan start.
// Without it, it is possilbe that onStopJob is called in another thread and
// closing out the CycledScanner
synchronized(ScanJob.this) {
if (mStopCalled) {
LogManager.d(TAG, "Quitting scan job before we even start. Somebody told us to stop.");
ScanJob.this.jobFinished(jobParameters , false);
return;
}

if (startedScan) {
LogManager.i(TAG, "Scan job running for "+mScanState.getScanJobRuntimeMillis()+" millis");
mStopHandler.postDelayed(new Runnable() {
@Override
public void run() {
LogManager.i(TAG, "Scan job runtime expired: " + ScanJob.this);
stopScanning();
mScanState.save();
ScanJob.this.jobFinished(jobParameters , false);
boolean startedScan;
if (mInitialized) {
LogManager.d(TAG, "Scanning already started. Resetting for current parameters");
startedScan = restartScanning();
}
else {
startedScan = startScanning();
}
mStopHandler.removeCallbacksAndMessages(null);

// need to execute this after the current block or Android stops this job prematurely
mStopHandler.post(new Runnable() {
if (startedScan) {
if (mScanState != null) {
LogManager.i(TAG, "Scan job running for "+mScanState.getScanJobRuntimeMillis()+" millis");
mStopHandler.postDelayed(new Runnable() {
@Override
public void run() {
scheduleNextScan();
}
});
LogManager.i(TAG, "Scan job runtime expired: " + ScanJob.this);
stopScanning();
mScanState.save();
ScanJob.this.jobFinished(jobParameters , false);

// need to execute this after the current block or Android stops this job prematurely
mStopHandler.post(new Runnable() {
@Override
public void run() {
scheduleNextScan();
}
});

}
}, mScanState.getScanJobRuntimeMillis());
}
}, mScanState.getScanJobRuntimeMillis());
}
else {
LogManager.i(TAG, "Scanning not started so Scan job is complete.");
ScanJob.this.jobFinished(jobParameters , false);
}
else {
LogManager.i(TAG, "Scanning not started so Scan job is complete.");
stopScanning();
mScanState.save();
LogManager.d(TAG, "ScanJob Lifecycle STOP (start fail): "+ScanJob.this);
ScanJob.this.jobFinished(jobParameters , false);
}
}

}
}).start();

return true;
}

private void scheduleNextScan(){
if(!mScanState.getBackgroundMode()){
// immediately reschedule scan if running in foreground
LogManager.d(TAG, "In foreground mode, schedule next scan");
ScanJobScheduler.getInstance().forceScheduleNextScan(ScanJob.this);
} else {
startPassiveScanIfNeeded();
if (mScanState != null) {
if(!mScanState.getBackgroundMode()){
// immediately reschedule scan if running in foreground
LogManager.d(TAG, "In foreground mode, schedule next scan");
ScanJobScheduler.getInstance().forceScheduleNextScan(ScanJob.this);
} else {
startPassiveScanIfNeeded();
}
}
}

private void startPassiveScanIfNeeded() {
LogManager.d(TAG, "Checking to see if we need to start a passive scan");
boolean insideAnyRegion = false;
// Clone the collection before iterating to prevent ConcurrentModificationException per #577
List<Region> regions = new ArrayList<>(mScanState.getMonitoringStatus().regions());
for (Region region : regions) {
RegionMonitoringState state = mScanState.getMonitoringStatus().stateOf(region);
if (state != null && state.getInside()) {
insideAnyRegion = true;
if (mScanState != null) {
LogManager.d(TAG, "Checking to see if we need to start a passive scan");
boolean insideAnyRegion = false;
// Clone the collection before iterating to prevent ConcurrentModificationException per #577
List<Region> regions = new ArrayList<>(mScanState.getMonitoringStatus().regions());
for (Region region : regions) {
RegionMonitoringState state = mScanState.getMonitoringStatus().stateOf(region);
if (state != null && state.getInside()) {
insideAnyRegion = true;
}
}
}
if (insideAnyRegion) {
// TODO: Set up a scan filter for not detecting a beacon pattern
LogManager.i(TAG, "We are inside a beacon region. We will not scan between cycles.");
}
else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mScanHelper.startAndroidOBackgroundScan(mScanState.getBeaconParsers());
if (insideAnyRegion) {
// TODO: Set up a scan filter for not detecting a beacon pattern
LogManager.i(TAG, "We are inside a beacon region. We will not scan between cycles.");
}
else {
LogManager.d(TAG, "This is not Android O. No scanning between cycles when using ScanJob");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mScanHelper != null) {
mScanHelper.startAndroidOBackgroundScan(mScanState.getBeaconParsers());
}
}
else {
LogManager.d(TAG, "This is not Android O. No scanning between cycles when using ScanJob");
}
}
}
}

@Override
public boolean onStopJob(JobParameters params) {
if (params.getJobId() == getPeriodicScanJobId(this)) {
LogManager.i(TAG, "onStopJob called for periodic scan " + this);
}
else {
LogManager.i(TAG, "onStopJob called for immediate scan " + this);
}
// Cancel the stop timer. The OS is stopping prematurely
mStopHandler.removeCallbacksAndMessages(null);
stopScanning();
startPassiveScanIfNeeded();
mScanHelper.terminateThreads();
// See corresponding synchronized block in onStartJob
synchronized(ScanJob.this) {
mStopCalled = true;
if (params.getJobId() == getPeriodicScanJobId(this)) {
LogManager.i(TAG, "onStopJob called for periodic scan " + this);
}
else {
LogManager.i(TAG, "onStopJob called for immediate scan " + this);
}
LogManager.d(TAG, "ScanJob Lifecycle STOP: "+ScanJob.this);
// Cancel the stop timer. The OS is stopping prematurely
mStopHandler.removeCallbacksAndMessages(null);

stopScanning();
startPassiveScanIfNeeded();
if (mScanHelper != null) {
mScanHelper.terminateThreads();
}
}
return false;
}

private void stopScanning() {
mInitialized = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mScanHelper.stopAndroidOBackgroundScan();
}
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
mScanHelper.getCycledScanner().destroy();
if (mScanHelper != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mScanHelper.stopAndroidOBackgroundScan();
}
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
mScanHelper.getCycledScanner().destroy();
}
}
LogManager.d(TAG, "Scanning stopped");
}

// Returns false if cycle thread cannot be allocated
// Returns false if ScanHelper cannot be initialized
private boolean initialzeScanHelper() {
mScanHelper = new ScanHelper(this);
mScanState = ScanState.restore(ScanJob.this);
mScanState.setLastScanStartTimeMillis(System.currentTimeMillis());
mScanHelper.setMonitoringStatus(mScanState.getMonitoringStatus());
mScanHelper.setRangedRegionState(mScanState.getRangedRegionState());
mScanHelper.setBeaconParsers(mScanState.getBeaconParsers());
mScanHelper.setExtraDataBeaconTracker(mScanState.getExtraBeaconDataTracker());
if (mScanHelper.getCycledScanner() == null) {
try {
mScanHelper.createCycledLeScanner(mScanState.getBackgroundMode(), null);
}
catch (OutOfMemoryError e) {
LogManager.w(TAG, "Failed to create CycledLeScanner thread.");
return false;
if (mScanState != null) {
ScanHelper scanHelper = new ScanHelper(this);
mScanState.setLastScanStartTimeMillis(System.currentTimeMillis());
scanHelper.setMonitoringStatus(mScanState.getMonitoringStatus());
scanHelper.setRangedRegionState(mScanState.getRangedRegionState());
scanHelper.setBeaconParsers(mScanState.getBeaconParsers());
scanHelper.setExtraDataBeaconTracker(mScanState.getExtraBeaconDataTracker());
if (scanHelper.getCycledScanner() == null) {
try {
scanHelper.createCycledLeScanner(mScanState.getBackgroundMode(), null);
}
catch (OutOfMemoryError e) {
LogManager.w(TAG, "Failed to create CycledLeScanner thread.");
return false;
}
}
mScanHelper = scanHelper;
}
else {
return false;
}
return true;
}

// Returns true of scanning actually was started, false if it did not need to be
private boolean restartScanning() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mScanHelper.stopAndroidOBackgroundScan();
}
long scanPeriod = mScanState.getBackgroundMode() ? mScanState.getBackgroundScanPeriod() : mScanState.getForegroundScanPeriod();
long betweenScanPeriod = mScanState.getBackgroundMode() ? mScanState.getBackgroundBetweenScanPeriod() : mScanState.getForegroundBetweenScanPeriod();
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().setScanPeriods(scanPeriod,
betweenScanPeriod,
mScanState.getBackgroundMode());
}
mInitialized = true;
if (scanPeriod <= 0) {
LogManager.w(TAG, "Starting scan with scan period of zero. Exiting ScanJob.");
if (mScanState != null && mScanHelper != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mScanHelper.stopAndroidOBackgroundScan();
}
long scanPeriod = mScanState.getBackgroundMode() ? mScanState.getBackgroundScanPeriod() : mScanState.getForegroundScanPeriod();
long betweenScanPeriod = mScanState.getBackgroundMode() ? mScanState.getBackgroundBetweenScanPeriod() : mScanState.getForegroundBetweenScanPeriod();
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
mScanHelper.getCycledScanner().setScanPeriods(scanPeriod,
betweenScanPeriod,
mScanState.getBackgroundMode());
}
mInitialized = true;
if (scanPeriod <= 0) {
LogManager.w(TAG, "Starting scan with scan period of zero. Exiting ScanJob.");
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
}
return false;
}
return false;
}

if (mScanHelper.getRangedRegionState().size() > 0 || mScanHelper.getMonitoringStatus().regions().size() > 0) {
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().start();
if (mScanHelper.getRangedRegionState().size() > 0 || mScanHelper.getMonitoringStatus().regions().size() > 0) {
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().start();
}
return true;
}
else {
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
}
return false;
}
return true;
}
else {
if (mScanHelper.getCycledScanner() != null) {
mScanHelper.getCycledScanner().stop();
}
return false;
}
}
Expand Down
Loading

0 comments on commit b3129cf

Please sign in to comment.