I use jetpack WorkManager to schedule a BLEScanWorker class to scan for BLE devices in the background every 15 minutes. BLEScanWorker scans BLE devices for 10 sec each time and saves the discovered BLE devices in a list. The code returns all the BLE devices perfectly when running on an Android 9 Samsung tablet with the app running in the foreground, hidden or closed and even after a restart. When running on an Android 13 Samsung Note S22 phone, it only returns the BLE devices correctly when the app is running in the foreground. It does not return any BLE devices when the app is hidden or closed.

Here are the codes to schedule the worker and the worker class. The compileSdkVersion is 33 in the build.gradle. The code was granted ACCESS_FINE_LOCATION, BLUETOOTH_SCAN and BLUETOOTH_ADMIN permissions.

Appreciate if someone can help to point out why the same code works on the Android 9 tablet but not the Android 13 phone. Thank you.

Constraints constraints = new Constraints.Builder()
                        .setRequiresCharging(false)
                        .setRequiresDeviceIdle(false)
                        .setRequiresBatteryNotLow(true)
                        .setRequiredNetworkType(NetworkType.NOT_REQUIRED)
                        .build();

PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(
                        BLEScanWorker.class, 15, TimeUnit.MINUTES)
                        .setConstraints(constraints)
                        .build();

mWorkManager.enqueueUniquePeriodicWork(
                        SCAN_WORK_NAME,
                        ExistingPeriodicWorkPolicy.KEEP, //Existing Periodic Work policy
                        periodicWorkRequest //work request
                );
public class BLEScanWorker extends Worker {

     private static final String TAG = BLEScanWorker.class.getName();
     private static final long SCAN_PERIOD = 10000;
     List<BluetoothDevice> mBLEDeviceList= new ArrayList<>();
     private Context mContext;

     public BLEScanWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);

        mContext = context;
    }

    private ScanCallback mScanCallback = new ScanCallback() {

        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);

            // save BLE device returned by result.getDevice() in mBLEDeviceList
            
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            super.onBatchScanResults(results);
            for (ScanResult result : results) {
               // save BLE device returned by result.getDevice() in mBLEDeviceList
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
            Log.e("onScanFailed", "Error code = " + errorCode);
        }
    };

    @NonNull
    @Override
    public Result doWork() {

        final BluetoothLeScanner bluetoothLeScanner;
        final ScanSettings settings;
        final List<ScanFilter> filters;

        BluetoothAdapter bluetoothAdapter;

        // It has been declared BLE required in the manifests.
        BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService (Context.BLUETOOTH_SERVICE);         
        if (bluetoothManager != null)
            bluetoothAdapter = bluetoothManager.getAdapter();
        else {
            Log.e(TAG, "failed to get BluetoothManager");
            return Result.failure();
        }

        try {
            bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
            settings = new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
                        .build();
            filters = new ArrayList<ScanFilter>();
        } catch (SecurityException se) {
            Log.e(TAG, se.getMessage());
            return Result.failure();
        }

        try {
            Log.d(TAG, Calendar.getInstance().getTime() + " " + "doWork Called");

            mBLEDeviceList.clear();

            try {
                bluetoothLeScanner.startScan(filters, settings, mScanCallback);
            } catch (SecurityException se) {
                Log.e(TAG, se.getMessage());
            }
            Log.i(TAG, Calendar.getInstance().getTime() + " start scan");

            Thread.sleep(SCAN_PERIOD); // sleep for 10 sec while scanning

            Log.i(TAG, Calendar.getInstance().getTime() + " stop scan");

            try {
                bluetoothLeScanner.stopScan(mScanCallback);
            } catch (SecurityException se) {
                Log.e(TAG, se.getMessage());
            }

            if (!mBLEDeviceList.isEmpty()) {
               // do something if the BLE device list is not empty
            }

            Log.i(TAG, "doWork finished");
            return Result.success();

        }
        catch (Throwable throwable)
        {
            Log.d(TAG, "Error Sending Notification " + throwable.getMessage());
            return Result.failure();
        }
    }
}

Below is the Android Studio log with the app running in the foreground. The BtGatt.GattService started scanning at 21:35:01 for 20 seconds and stopped at 21:35:21. The scanner successfully found BLE devices. The scanner started and stopped once as expected.

enter image description here

Below is the Android Studio log with the app hidden. The BtGatt.GattService started scanning at 21:50:37 for 20 seconds and stopped at 21:50:57. The scanner failed to find any BLE devices. The scanner started and stopped expectedly multiple times.

enter image description here

2

There are 2 best solutions below

0
On

Only one permission is needed for Android 12+ devices for BLE scan

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                     android:usesPermissionFlags="neverForLocation" /> 

neverForLocation flag asserts that the BLE scan is not performed to get location from BLE device. Enabling this flag will omitt BLE beacons from scan result.

Once the permission is added in the manifest, request the permission at runtime or enable Nearby device permission from app settings.

0
On

On an Android 13 Samsung Note S22 phone, when running the app in the background or when the app is closed, the periodic worker only works correctly only with

included in the manifest and user allows location permission all the time in the app setting.

According to https://developer.android.com/training/location/permissions#request-background-location, "On Android 10 (API level 29) and higher, you must declare the ACCESS_BACKGROUND_LOCATION permission in your app's manifest in order to request background location access at runtime. On earlier versions of Android, when your app receives foreground location access, it automatically receives background location access as well."