Repository: altbeacon/android-beacon-library
Issue: ANRs due to the ScanState.restore being called on the main thread

In our app we listen to the bluetooth state changes when the app is in the foreground so that we can stop foreground beacon scans manually and start them again once the Bluetooth is on again. 

Some time ago we started noticing ANRs happening on rare occasions, the relevant part of stackrace (not sure if this term is applicable to the ANRs though) looks like this:

```
"main" tid=1 Runnable Performing disk I/O
"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x73fa1a78 self=0x7d37414c00
  | sysTid=11757 nice=-10 cgrp=default sched=0/0 handle=0x7dbcde5548
  | state=R schedstat=( 503793695236 32744365477 307265 ) utm=31364 stm=19014 core=3 HZ=100
  | stack=0x7fea04b000-0x7fea04d000 stackSize=8MB
  | held mutexes= "mutator lock"(shared held)
  #00  pc 00000000003c8d04  /system/lib64/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+220)
  #01  pc 0000000000498b90  /system/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool, BacktraceMap*, bool) const+352)
  #02  pc 00000000004b29a4  /system/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+828)
  #03  pc 0000000000499860  /system/lib64/libart.so (art::Thread::RunCheckpointFunction()+204)
  #04  pc 00000000003776f8  /system/lib64/libart.so (art::JNI::GetByteArrayElements(_JNIEnv*, _jbyteArray*, unsigned char*)+1316)
  #05  pc 000000000002f744  /system/lib64/libjavacore.so (Linux_readBytes(_JNIEnv*, _jobject*, _jobject*, _jobject*, int, int)+128)
  at libcore.io.Linux.readBytes (Linux.java)
  at libcore.io.Linux.read (Linux.java:184)
  at libcore.io.BlockGuardOs.read (BlockGuardOs.java:254)
  at libcore.io.IoBridge.read (IoBridge.java:501)
  at java.io.FileInputStream.read (FileInputStream.java:307)
  at java.io.FileInputStream.read (FileInputStream.java:245)
  at java.io.ObjectInputStream$PeekInputStream.peek (ObjectInputStream.java:2312)
  at java.io.ObjectInputStream$BlockDataInputStream.peek (ObjectInputStream.java:2605)
  at java.io.ObjectInputStream$BlockDataInputStream.peekByte (ObjectInputStream.java:2615)
  at java.io.ObjectInputStream.readClassDesc (ObjectInputStream.java:1508)
  at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1776)
  at java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1353)
  at java.io.ObjectInputStream.defaultReadFields (ObjectInputStream.java:2002)
  at java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:1926)
  at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1803)
  at java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1353)
  at java.io.ObjectInputStream.readObject (ObjectInputStream.java:373)
  at java.util.HashSet.readObject (HashSet.java:333)
  at java.lang.reflect.Method.invoke (Method.java)
  at java.io.ObjectStreamClass.invokeReadObject (ObjectStreamClass.java:1067)
  at java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:1902)
  at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1803)
  at java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1353)
  at java.io.ObjectInputStream.defaultReadFields (ObjectInputStream.java:2002)
  at java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:1926)
  at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1803)
  at java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1353)
  at java.io.ObjectInputStream.readObject (ObjectInputStream.java:373)
  at org.altbeacon.beacon.service.ScanState.restore (ScanState.java)
- locked <0x02c2bafd> (a java.lang.Class<org.altbeacon.beacon.service.ScanState>)
  at org.altbeacon.beacon.service.ScanJobScheduler.applySettingsToScheduledJob (ScanJobScheduler.java)
  at org.altbeacon.beacon.BeaconManager.applyChangesToServices (BeaconManager.java)
  at org.altbeacon.beacon.BeaconManager.startMonitoringBeaconsInRegion (BeaconManager.java)
  at org.altbeacon.beacon.startup.RegionBootstrap$InternalBeaconConsumer.onBeaconServiceConnect (RegionBootstrap.java)
  at org.altbeacon.beacon.BeaconManager.bind (BeaconManager.java)
- locked <0x09cec503> (a java.util.concurrent.ConcurrentHashMap)
  at org.altbeacon.beacon.startup.RegionBootstrap.<init> (RegionBootstrap.java)
```

This does not affect ALL devices though - we were not able to reproduce it on some Xiaomi devices we have and my own Nexus 5 does not seem to be affected either (I guess it could be related to the fact that ScanState.restore invocation is guarded by the `mScheduledScanJobsEnabled` and this variable is equal to true only if OS version >= Oreo).

### Expected behavior
The ANR does not occur.

### Actual behavior
The app literally hangs and stops responding to the user input. Sometimes it leads to the ANR, sometimes the app starts listening to user input within 2-3 seconds. 

### Steps to reproduce this behavior
The way we're able to reproduce it is following:
1. Stand close to the ibeacon so that the app is able to see it.
2. Open the notifications drawer, turn Bluetooth off, then turn it on.
3. App hangs and does not respond. Sometimes it leads to ANR, sometimes the app is able to return to normal state.

### Mobile device model and OS version
Google Pixel 
Google Pixel 2 XL
Samsung S8+
### Android Beacon Library version
2.15.4

IMPORTANT:  This forum is reserved for feature requests or reproducible bugs with the library itself.  If you need help with using the library with your project, please open a new question on StackOverflow.com.


Changed files:
lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java

diff --git a/lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java b/lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java
index 18b6665..b13e0e9 100644
--- a/lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java
+++ b/lib/src/main/java/org/altbeacon/beacon/service/ScanJob.java
@@ -54,63 +54,71 @@ public class ScanJob extends JobService {
 
     @Override
     public boolean onStartJob(final JobParameters jobParameters) {
-        if (!initialzeScanHelper()) {
-            LogManager.e(TAG, "Cannot allocate a scanner to look for beacons.  System resources are low.");
-            return false;
-        }
-        ScanJobScheduler.getInstance().ensureNotificationProcessorSetup(getApplicationContext());
-        if (jobParameters.getJobId() == getImmediateScanJobId(this)) {
-            LogManager.i(TAG, "Running immediate scan job: instance is "+this);
-        }
-        else {
-            LogManager.i(TAG, "Running periodic scan job: instance is "+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());
-            }
-        }
-        LogManager.d(TAG, "Done processing queued scan resuilts");
+        // 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.
+        new Thread(new Runnable() {
+            public void run() {
+                if (!initialzeScanHelper()) {
+                    LogManager.e(TAG, "Cannot allocate a scanner to look for beacons.  System resources are low.");
+                    ScanJob.this.jobFinished(jobParameters , false);
+                }
+                ScanJobScheduler.getInstance().ensureNotificationProcessorSetup(getApplicationContext());
+                if (jobParameters.getJobId() == getImmediateScanJobId(ScanJob.this)) {
+                    LogManager.i(TAG, "Running immediate scan job: instance is "+this);
+                }
+                else {
+                    LogManager.i(TAG, "Running periodic scan job: instance is "+this);
+                }
 
-        boolean startedScan;
-        if (mInitialized) {
-            LogManager.d(TAG, "Scanning already started.  Resetting for current parameters");
-            startedScan = restartScanning();
-        }
-        else {
-            startedScan = startScanning();
-        }
-        mStopHandler.removeCallbacksAndMessages(null);
+                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());
+                    }
+                }
+                LogManager.d(TAG, "Done processing queued scan resuilts");
 
-        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) {
+                    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.");
+                    ScanJob.this.jobFinished(jobParameters , false);
+                }
+            }
+        }).start();
+
         return true;
     }