3

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.

user2988
  • 95
  • 1
  • 7
  • 1
    You use a `foreground service` for stuff that needs to persist after your app is in the background – tyczj Aug 30 '18 at 20:38
  • For this implementation I was hoping to avoid having to use a foreground service. There are some vendors that appear to be getting this done without a foreground service as well. For example, I have JayBird Run wireless headphones. When I take them out of their case, my phone recognizes the BLE advertisement within ~10 seconds and connects. JayBird does NOT have a foreground service running. How are they doing this? – user2988 Aug 30 '18 at 20:43
  • 1
    your headphones are not an app, your phone has a bluetooth service that is always running that will connect to headphones and such – tyczj Aug 30 '18 at 20:49
  • But how are they establishing the connecting phone-side in the first place such that it will auto connect on new advertisement? I've tried using the connectGatt method with autoConnect set to true -- connection either never happens or takes an agonizingly long time. – user2988 Aug 30 '18 at 20:53
  • 2
    I dont understand what you mean, the headphones connect to the phone's bluetooth service then the audio gets routed to the headphones, an app just needs to play audio to the media player they dont need to connect to the actual device – tyczj Aug 30 '18 at 20:55
  • Regarding Dual Mode (BR+EDR/BLE) BT headphones: - I believe they advertise and provide some information via GATT (BLE [BT 4, 4.2, 5...] to save power) - Audio transmission is done via A2DP [Profile] (BR/EDR [BT 2+EDR, 2.1+EDR] for better audio quality.) In Android Q, this is becoming streamlined. – Zachary Moshansky Jun 06 '19 at 02:12

1 Answers1

11

WorkManager is not appropriate for continuous work - that would be the use case for foreground services.

However, BLE scanning does not require your app to be continuously running on API 26+ devices with the introduction of the BluetoothLeScanner.startScan(List<ScanFilter>, ScanSettings, PendingIntent) method, which allows you to register a PendingIntent as a callback, starting your app only when scan results are available.

For previous versions of Android, you would need a constantly running service to maintain the active scanning.

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • Ah, missed that startScan overload. Exactly what I need, thanks! – user2988 Aug 30 '18 at 23:27
  • I tried the `PendingIntent` as described. It worked when called from the main Activity, but of course the scanning stopped when I killed the app. With `WorkManager` I'm able to perform background task such as server updates every 2-20 minutes (as decided by the OS). I hope to be able to extend my `Worker` with a brief BLE scanning. So I moved the `startScan` with the `PendingIntent` to my `Worker` class - kept the receiver and it's registration in the main Activity. I get no exceptions, but no scans back either. As n00b to Intents - anyone's got an idea to what's wrong, or some code to share? – luposlip Sep 06 '18 at 20:18
  • Force stopping an app stops everything until the user opens your app, that's WAI and very, very different from swiping the task away in the Recents screen, for instance. If you're having problems with your callback, you should create a new question. – ianhanniballake Sep 06 '18 at 20:22
  • To clarify I do swipe away the app, I don't force stop it from settings (as my Worker keeps running). But fair point, I'll ask a different question. – luposlip Sep 06 '18 at 20:28
  • http://www.davidgyoungtech.com/2017/08/07/beacon-detection-with-android-8 – Vladimir Petrovski Oct 04 '18 at 12:46
  • What are the examples of a constantly running service for previous Android versions? Foreground service, Sticky service or something else? – Parth Parekh Jul 19 '19 at 20:26
  • 1
    @ParthParekh - you'd use a foreground service for anything that needs to be constantly running. That's true on all API levels. – ianhanniballake Jul 19 '19 at 21:14
  • @ianhanniballake do you know if this will trigger the "the application has accessed your location in the background" notification? – Maragues Feb 25 '20 at 11:23
  • Hi mates, finally how did you accomplished the task? I've been quite outdated in Android and I would like to open my app as soon as a bluetooth device is found. Should I use a Worker or a Foreground service always running (and showing notif) and scanning? – MarcForn Feb 05 '23 at 19:47