Bluetooth LE Scan fails in the background - permissions
Asked Answered
B

4

29

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?

Brushwork answered 22/9, 2015 at 4:26 Comment(1)
Do you need both - NETWORK_PROVIDER and GPS_PROVIDER? Or just NETWORK_PROVIDER. More info here: developer.android.com/guide/topics/connectivity/…Tanney
B
35

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

Brushwork answered 23/9, 2015 at 2:50 Comment(9)
I added <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> to my manifest but I receive the error message: "java.lang.SecurityException: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to get scan results". Any idea how to solve this?Calise
Starting with Android 6, you must also present a permission request to a usr at runtime. See here for an example: developer.radiusnetworks.com/2015/09/29/…Brushwork
@Brushwork when i give permission it stops to scan beacons. Please help to to get out of this problem.Hebraism
Please create a new question showing the code you use and describing more details about the problem.Brushwork
@Brushwork For Android 10, do we need to ask runtime permissions for both ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION along with ACCESS_BACKGROUND_LOCATION?Lambkin
No, if you declare both in the manifest and then request one permission at runtime, it will prompt the user with both options (always or while in use). Even though there is one dialog, be aware that the user may grant location permission but not background location permission.Brushwork
Adding 'ACCESS_BACKGROUND_LOCATION' solved the problem although it says: 'ACCESS_FINE_LOCATION' which was already in the manifest.Pieter
Why do we need ACCESS_BLUETOOTH_ADMIN?Tanney
You have to ask Google to be sure why they designed it that way. But originally the only app to do Bluetooth scanning was the system widget managing Bluetooth pairings. From a historical perspective that might have made sense at the time.Brushwork
L
33

I had a similar problem with an app connecting to bluetooth. Not LE ScanFilter, but it was a permissions issue just like the OP had.

Root cause is that starting with SDK 23, you need to prompt the user for permissions at runtime using Activity's requestPermissions() method.

Here's what worked for me:

  1. Add one of the following two lines to AndroidManifest.xml, inside the root node:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
  2. In your Activity, before attempting to connect to bluetooth, call Activity's requestPermissions() method, which opens a system dialog to prompt the user for the permission. The permissions dialog opens in a different thread, so be sure to wait for the result before trying to connect to bluetooth.

  3. Override Activity's onRequestPermissionsResult() to handle the result. This method will really only need to do something if the user refused to grant the permission, to tell the user that the app can't do the bluetooth activity.

This blog post has some example code that uses AlertDialogs to tell the user what's going on. It is a good starting point but has some shortcomings:

  • It doesn't handle waiting for the requestPermissions() thread to finish
  • The AlertDialog wrapping the call to requestPermissions() seems extraneous to me. A bare call to requestPermissions() is sufficient.
Licorice answered 3/5, 2016 at 22:54 Comment(1)
Can we get BLE readings without requesting ACCESS_COARSE_LOCATION?Tanney
S
14

Add location permission along with BLE

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Copy Paste this method to request and grant location permission

  @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
                Map<String, Integer> perms = new HashMap<String, Integer>();
                // Initial
                perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);


                // Fill with results
                for (int i = 0; i < permissions.length; i++)
                    perms.put(permissions[i], grantResults[i]);

                // Check for ACCESS_FINE_LOCATION
                if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED

                        ) {
                    // All Permissions Granted

                    // Permission Denied
                    Toast.makeText(ScanningActivity.this, "All Permission GRANTED !! Thank You :)", Toast.LENGTH_SHORT)
                            .show();


                } else {
                    // Permission Denied
                    Toast.makeText(ScanningActivity.this, "One or More Permissions are DENIED Exiting App :(", Toast.LENGTH_SHORT)
                            .show();

                    finish();
                }
            }
            break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }


    @TargetApi(Build.VERSION_CODES.M)
    private void stuffMarshMallow() {
        List<String> permissionsNeeded = new ArrayList<String>();

        final List<String> permissionsList = new ArrayList<String>();
        if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
            permissionsNeeded.add("Show Location");

        if (permissionsList.size() > 0) {
            if (permissionsNeeded.size() > 0) {

                // Need Rationale
                String message = "App need access to " + permissionsNeeded.get(0);

                for (int i = 1; i < permissionsNeeded.size(); i++)
                    message = message + ", " + permissionsNeeded.get(i);

                showMessageOKCancel(message,
                        new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                        REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                            }
                        });
                return;
            }
            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
            return;
        }

        Toast.makeText(ScanningActivity.this, "No new Permission Required- Launching App .You are Awesome!!", Toast.LENGTH_SHORT)
                .show();
    }

    private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(ScanningActivity.this)
                .setMessage(message)
                .setPositiveButton("OK", okListener)
                .setNegativeButton("Cancel", null)
                .create()
                .show();
    }

    @TargetApi(Build.VERSION_CODES.M)
    private boolean addPermission(List<String> permissionsList, String permission) {

        if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
            permissionsList.add(permission);
            // Check for Rationale Option
            if (!shouldShowRequestPermissionRationale(permission))
                return false;
        }
        return true;
    }

And then in onCreate check for permission

 if (Build.VERSION.SDK_INT >= 23) {
            // Marshmallow+ Permission APIs
            stuffMarshMallow();
        }

Hope it save your time.

Scatter answered 17/5, 2017 at 10:15 Comment(0)
I
2

If your app targets Android Q, it's not enough with only coarse location, you need to use fine location, otherwise you will get this error:

E/BluetoothUtils: Permission denial: Need ACCESS_FINE_LOCATION permission to get scan results

See https://developer.android.com/preview/privacy/camera-connectivity#fine-location-telephony-wifi-bt for the official source.

Incisure answered 21/8, 2019 at 17:17 Comment(2)
I guess using Fine location only would do the trick, as it covers Coarse location specs with extra GPS location detection coverage!Yevette
ya, the main point is we need to have both of them instead of one of them, thanksProlusion

© 2022 - 2024 — McMap. All rights reserved.