Search code examples
androidbluetooth-lowenergyandroid-bluetoothandroid-6.0-marshmallow

Bluetooth LE Scan fails in the background - permissions


The following code works great on my Nexus 9 running Android 5.1.1 (Build LMY48M), but won't work on a Nexus 9 running Android 6.0 (Build MPA44l)

List<ScanFilter> filters = new ArrayList<ScanFilter>();
ScanSettings settings = (new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)).build();
ScanFilter.Builder builder = new ScanFilter.Builder();
builder.setManufacturerData((int) 0x0118, new byte[]{(byte) 0xbe, (byte) 0xac}, new byte[]{(byte) 0xff, (byte)0xff});
ScanFilter scanFilter = builder.build();
filters.add(scanFilter);
mBluetoothLeScanner.startScan(filters, settings, new ScanCallback() {
  ...
});

On Android 5.x, the above code yields a callback when a manufacturer advertisement matching the scan filter is seen. (See example Logcat output below.) On the Nexus 9 with MPA44l, no callbacks are received. If you comment out the scan filter, callbacks are received successfully on the Nexus 9.

09-22 00:07:28.050    1748-1796/org.altbeacon.beaconreference D/BluetoothLeScanner﹕ onScanResult() - ScanResult{mDevice=00:07:80:03:89:8C, mScanRecord=ScanRecord [mAdvertiseFlags=6, mServiceUuids=null, mManufacturerSpecificData={280=[-66, -84, 47, 35, 68, 84, -49, 109, 74, 15, -83, -14, -12, -111, 27, -87, -1, -90, 0, 1, 0, 1, -66, 0]}, mServiceData={}, mTxPowerLevel=-2147483648, mDeviceName=null], mRssi=-64, mTimestampNanos=61272522487278}

Has anybody seen ScanFilters work on Android M?


Solution

  • The problem was not the scan filter, but background permissions.

    Android 10-11:

    In order to detect BLE devices in the background, you must have several permissions in the manifest. Place the following in your AndroidManifest.xml:

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    

    Then add code like follows to your Activity to dynamically request these permissions from the user:

        private static final int PERMISSION_REQUEST_FINE_LOCATION = 1;
        private static final int PERMISSION_REQUEST_BACKGROUND_LOCATION = 2;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
    
    
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (this.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                        == PackageManager.PERMISSION_GRANTED) {
                    if (this.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
                            != PackageManager.PERMISSION_GRANTED) {
                        if (this.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
                            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                            builder.setTitle("This app needs background location access");
                            builder.setMessage("Please grant location access so this app can detect beacons in the background.");
                            builder.setPositiveButton(android.R.string.ok, null);
                            builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
    
                                @TargetApi(23)
                                @Override
                                public void onDismiss(DialogInterface dialog) {
                                    requestPermissions(new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                                            PERMISSION_REQUEST_BACKGROUND_LOCATION);
                                }
    
                            });
                            builder.show();
                        }
                        else {
                            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                            builder.setTitle("Functionality limited");
                            builder.setMessage("Since background location access has not been granted, this app will not be able to discover beacons in the background.  Please go to Settings -> Applications -> Permissions and grant background location access to this app.");
                            builder.setPositiveButton(android.R.string.ok, null);
                            builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
    
                                @Override
                                public void onDismiss(DialogInterface dialog) {
                                }
    
                            });
                            builder.show();
                        }
    
                    }
                } else {
                    if (!this.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
                        requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION,
                                        Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                                PERMISSION_REQUEST_FINE_LOCATION);
                    }
                    else {
                        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                        builder.setTitle("Functionality limited");
                        builder.setMessage("Since location access has not been granted, this app will not be able to discover beacons.  Please go to Settings -> Applications -> Permissions and grant location access to this app.");
                        builder.setPositiveButton(android.R.string.ok, null);
                        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
    
                            @Override
                            public void onDismiss(DialogInterface dialog) {
                            }
    
                        });
                        builder.show();
                    }
    
                }
            }
        }
    

    When you prompt the user for location permission, the OS dialog will give them the option to downgrade that permission request to "Allow Only While Using the App" vs. "Allow All the Time". If the user chooses the first option, you will not get detections in the background, even if everything else above is set up.

    On Android 11, things get more complex still, as the OS offers yet another option of "Only this time" for the permission request. If your app targets SDK 30 (Android 11), it won't even offer the user the option for "Allow All the Time", and the user will have to go to Settings as a separate step to turn on all the time access. See here for more details on the way this works on Android 11.

    For a broader discussion of the evolution of permissions prompting, see my blog post here.

    Before Android 10:

    Starting with Android M, Bluetooth LE scanning in the background is blocked unless the app has one of the following two permissions:

    android.permission.ACCESS_COARSE_LOCATION
    android.permission.ACCESS_FINE_LOCATION
    

    The app I was testing did not request either of these permissions, so it did not work in the background (the only time the scan filter was active) on Android M. Adding the first one solved the problem.

    I realized this was the problem because I saw the following line in Logcat:

    09-22 22:35:20.152  5158  5254 E BluetoothUtils: Permission denial: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to get scan results
    

    See here for details: https://code.google.com/p/android-developer-preview/issues/detail?id=2964