With the recent restrictions to background Services and implicit Broadcasts, Android devs are left with JobScheduler, and, at a higher level, WorkManager to schedule background tasks.
The Worker class for WorkManager is simple enough, but I'm a bit confused about the best way to implement ongoing work as opposed to one-off work. For our example, let's consider Bluetooth Low Energy scanning, but the same concern applies to all ongoing, indeterminate work.
Something like this obviously doesn't work:
public class MyWorker extends Worker {
private BluetoothLeScanner mBluetoothLeScanner;
@Override
public Worker.Result doWork() {
mBluetoothLeScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
// Pretend there's some bluetooth setup here
// ...
mBluetoothLeScanner.startScan( .. , .. , .. );
return Result.SUCCESS;
}
}
Above we start scanning, then fall out of scope immediately, so scanning will not continue.
We can use wait()/notify() to get around this, but it feels very dirty. Something like this...
public class MyWorker extends Worker {
private BluetoothLeScanner mBluetoothLeScanner;
private final Object mLock = new Object();
private Handler mBackgroundHandler;
private Handler getBackgroundHandler() {
if (mBackgroundHandler == null) {
HandlerThread thread = new HandlerThread("background");
thread.start();
mBackgroundHandler = new Handler(thread.getLooper());
}
return mBackgroundHandler;
}
@Override
public Worker.Result doWork() {
getBackgroundHandler().post(new Runnable() {
@Override
public void run() {
mBluetoothLeScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
// Pretend there's some bluetooth setup here
// ...
mBluetoothLeScanner.startScan( .. , .. , mScanCallback);
}
});
getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
mBluetoothLeScanner.stopScan(mScanCallback);
synchronized (mLock) {
mLock.notify();
}
}
}, 60 * 1000); //stop after a minute
try {
synchronized (mLock) {
mLock.wait();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return Result.SUCCESS;
}
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//We found an advertisement
mBluetoothLeScanner.stopScan(mScanCallback);
synchronized (mLock) {
mLock.notify();
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
mBluetoothLeScanner.stopScan(mScanCallback);
synchronized (mLock) {
mLock.notify();
}
}
};
@Override
public void onStopped(boolean cancelled) {
if (mBackgroundHandler != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mBackgroundHandler.getLooper().quitSafely();
} else {
mBackgroundHandler.getLooper().quit();
}
mBackgroundHandler = null;
}
}
}
TLDR: What is the best way to implement ongoing background work in modern (8.1+) Android? It does appear, given the architecture of Worker/WorkManager, that this kind of ongoing background work is being snuffed out by Google. Is a wait()/notify() pattern in a Worker acceptable, or will this workaround get killed by the system?
Any tips would be appreciated.
Edit:
I was hoping to avoid using a foreground Service + ongoing notification. The answer here seemed promising, but it was apparently patched in Android 7.1. On my phone running Android 9, my wireless BLE headphones connect almost immediately when taken out of their case. The headphone vendor is NOT running a foreground service (at least not visibly -- there is no persistent notification) to detect the advertisement. I have no idea how they're doing this so reliably.