M.Sudi
M.Sudi

Reputation: 23

Android - No BLE devices can be found after scanning a few times in a row

We have created an Android app which scans for specific manufacturer devices by using BLE (Bluetooth Low Energy). Actually everything is working fine, but we have one problem. Sometimes after a few scans the tablet can no longer find any devices in all scans that follow. The only one workaround is to turn Bluetooth off and on again, then the tablet can find devices again. The environment has about 30 BLE devices and 20 of them are the devices we would like to find and filter. The client can reproduce it but we unfortunately can not so it is hard to debug.

The scan should only work in foreground, there is not need to scan in background. The user can restart the scan just after the scan ends. I already know the 30 seconds / 5 scans limit, this is handled fine.

The target tablet is running Android 8.1, the project settings are:

  compileSdkVersion = 28
  buildToolsVersion = '30.0.2'
  minSdkVersion = 17
  targetSdkVersion = 28

BLE Scan Settings:

ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
    .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
    .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
    .setReportDelay(0L)
    .build();

new ScanFilter.Builder().setManufacturerData(MANUFACTURE_ID, new byte[0]).build());

When the Scan is working, Logs looks like this:

I/BleScanService: Start Scanning
D/Fragment: refreshUIElements true
D/BluetoothAdapter: isLeEnabled(): ON
D/BluetoothLeScanner: onScannerRegistered() - status=0 scannerId=7 mScannerId=0
I/Fragment$ScannerBroadcastReceiver: listing device BLE: deviceMacAddress:EC:A5:80:12:D4:1A, nwpBleMacAddress:XXXXXXXXXXEE, name:Device
I/Fragment$ScannerBroadcastReceiver: listing device BLE: deviceMacAddress:CE:A8:80:60:C9:A8, nwpBleMacAddress:XXXXXXXXXX08, name:Device
D/BluetoothAdapter: isLeEnabled(): ON
I/BleScanService: Stopped Scanning

When the scan stops working, i get logs like this (interesting is that the isLeEnabled(): ON log entry no longer appears):

07-28 14:02:48:310 I/BleScanService(2) : Start Scanning
07-28 14:02:48:316 I/BleScanService(2) : Resume Scanning in 0 ms
07-28 14:02:48:324 D/Fragment(2) : refreshUIElements true
07-28 14:02:54:357 I/BleScanService(2) : Stopped Scanning

This is the ScanService class which does all the magic:

public class BleScanService {

    private final Handler scanHandler;
    private final Context context;
    private final Settings settings;
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothLeScanner bluetoothLeScanner;

    private final Set<String> discoveredDevices;
    private final List<Long> scanTimestamps = new ArrayList<>(10);

    private boolean scanning;

    @Inject
    public BleScanService(Context context, Settings settings) {
        this.context = context;
        this.settings = settings;
        this.scanHandler = new Handler();
    }

    public BluetoothDevice getBluetoothDevice(String deviceMacAddress) {
        return bluetoothAdapter.getRemoteDevice(deviceMacAddress);
    }

    public boolean isScanning() {
        return scanning;
    }

    public void startScanning() {
        if (scanning) {
            Timber.i("Ignore start scanning request because scanning is already active");
            return;
        }

        try {
            scanning = true;
            discoveredDevices.clear();
            initService();

            long nextScannerAvailability = getNextScannerAvailability();
            Timber.i("Resume Scanning in %s ms", nextScannerAvailability);

            scanHandler.postDelayed(this::scan, nextScannerAvailability);

        } catch (Exception e) {
            Timber.e(e);
            stopScanning();
        }
    }

    private void scan() {
        bluetoothLeScanner.startScan(BleScanSettings.getManufacturerScanFilter(), BleScanSettings.getScanSettings(), leScanCallback);
        scanHandler.postDelayed(() -> {
            stopScanning();
            Timber.i("Devices shown %s", String.join(", ", discoveredDevices));
        }, 8000);
    }

    private void initService() {
        if (bluetoothAdapter == null) {
            BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
            if (bluetoothManager != null) {
                bluetoothAdapter = bluetoothManager.getAdapter();
            }
            if (bluetoothLeScanner == null) {
                bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
            }
        }
    }

    private long getNextScannerAvailability() {
        final long currentTimeMillis = System.currentTimeMillis();
        scanTimestamps.removeIf(t -> currentTimeMillis - t > 30000);

        if (scanTimestamps.size() < 4) {
            return 0;
        }
        long oldestActiveTimestamp = scanTimestamps.get(scanTimestamps.size() - 4);

        return 30000 - (currentTimeMillis - oldestActiveTimestamp) + 500;
    }

    public void stopScanning() {
        if (bluetoothAdapter == null) {
            return;
        }

        try {
            bluetoothLeScanner.stopScan(leScanCallback);
        } catch (Exception e) {
            Timber.w(e, "Exception occurred while attempting to stop BLE scanning.");
        }
        if (scanning) {
            scanTimestamps.add(System.currentTimeMillis());
        }

        scanHandler.removeCallbacksAndMessages(null);
        scanning = false;
        Timber.i("Stopped Scanning");
    }

    protected void broadcastDetectedDevice(Intent intent) {
        context.sendBroadcast(intent);
    }

    private final ScanCallback leScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            if (!scanning) {
                Timber.i("ignore scanning result because the scanning is paused");
                return;
            }

            if (result.getScanRecord() == null) {
                Timber.i("Skip unsupported device %s", result.getDevice().getName());
                return;
            }

            BluetoothDevice device = result.getDevice();
            String deviceMacAddress = device.getAddress();

            if (isAlreadyDiscovered(deviceMacAddress)) {
                return;
            }

            byte[] manufacturerSpecificData = result.getScanRecord().getManufacturerSpecificData(BleScanSettings.MANUFACTURE_ID);
            final Optional<Intent> msdIntent = BleDetectedDeviceIntentHelper
                    .getIntentFromManufacturerSpecificData(deviceMacAddress, manufacturerSpecificData);

            if (msdIntent.isPresent()) {
                handleManufacturerDataScan(deviceMacAddress, msdIntent.get());
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
            Timber.i("SCAN FAILED");
            stopScanning();
        }

Permissions which are used and also requested:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.CAMERA" />

Any ideas? Thank you for your help!

Upvotes: 2

Views: 2283

Answers (1)

matdev
matdev

Reputation: 4283

The behavior you describe happens when an app scans too frequently i.e. more than 5 times per 30 seconds. It seems you are scanning too frequently for some reason...

Stopping and re-starting scan automatically after the 8 secs delay may solve the problem.

Also, from Android 12+, apps require BLUETOOTH_SCAN permission in order to scan for Bluetooth devices.

Add this line to your app manifest:

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

Here is a sample code to request the permission:

public void requestBluetoothPermissions() {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

        if (this.checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),
                    Manifest.permission.BLUETOOTH_SCAN)) {

                 // display permission rationale in snackbar or dialog message 
            } else {

                this.requestPermissions(new String[]{
                    Manifest.permission.BLUETOOTH_SCAN
                    }, MyActivity.REQUEST_BLUETOOTH_PERMISSIONS);
            }
        }
    }
}

Upvotes: 1

Related Questions