蓝牙低功耗扫描在后台失败-权限

29

以下代码在我的 Nexus 9 上运行良好,其运行的 Android 版本为 5.1.1(Build LMY48M),但在运行 Android 6.0(Build MPA44l)的 Nexus 9 上无法正常工作。

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() {
  ...
});

在Android 5.x上,以上代码会在扫描过滤器匹配到厂商广告时产生回调(见下面例子的Logcat输出)。但在搭载MPA44l的Nexus 9上则不会收到任何回调。如果注释掉扫描过滤器,则在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}

有人看到ScanFilters在Android M上工作吗?


你需要同时使用NETWORK_PROVIDER和GPS_PROVIDER吗?还是只需要NETWORK_PROVIDER。更多信息请参考这里:https://developer.android.com/guide/topics/connectivity/bluetooth-le.html - IgorGanapolsky
4个回答

35

问题不在于扫描过滤器,而在于后台权限。

Android 10-11:

为了在后台检测BLE设备,必须在清单文件中具有多个权限。请将以下内容放置在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" />

然后在您的Activity中添加以下代码,以动态请求用户这些权限:

    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();
                }

            }
        }
    }

当您提示用户获取位置权限时,操作系统对话框会给他们选择将该权限请求降级为“仅在使用应用程序时允许”还是“始终允许”的选项。如果用户选择第一个选项,则即使以上所有设置都已完成,您也不会在后台收到检测结果。

在Android 11上,情况变得更加复杂,因为操作系统提供了另一种权限请求的选项:“仅此一次”。如果您的应用程序针对SDK 30(Android 11),它甚至不会向用户提供“始终允许”的选项,用户将不得不单独转到设置中打开全天候访问权限。有关Android 11上这种工作方式的更多详细信息,请参见这里

有关权限提示演变的更广泛讨论,请参阅我的博客文章这里

Android 10之前:

从Android M开始,除非应用程序具有以下两个权限之一,否则将禁止在后台进行蓝牙LE扫描:

android.permission.ACCESS_COARSE_LOCATION
android.permission.ACCESS_FINE_LOCATION

我测试的应用程序没有请求这两个权限,因此它在Android M上不会在后台工作(只有扫描过滤器处于活动状态时)。添加第一个权限解决了问题。

我之所以意识到这是问题,是因为我在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

详情请查看:https://code.google.com/p/android-developer-preview/issues/detail?id=2964


12
我在清单文件中添加了<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>,但我收到了错误信息:"java.lang.SecurityException: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to get scan results"。有什么解决办法吗? - Eidan Spiegel
5
从Android 6开始,您还必须在运行时向用户提出权限请求。请参见以下示例:http://developer.radiusnetworks.com/2015/09/29/is-your-beacon-app-ready-for-android-6.html - davidgyoung
@davidgyoung 当我给予权限时,它停止扫描信标。请帮助我解决这个问题。 - Viks
为什么我们需要 ACCESS_BLUETOOTH_ADMIN - IgorGanapolsky
你必须询问Google以确保它们为什么以那种方式设计。但最初执行蓝牙扫描的唯一应用程序是管理蓝牙配对的系统小部件。从历史角度来看,这在当时可能是有意义的。 - davidgyoung
显示剩余4条评论

33

我曾经遇到一个类似的问题,是一个应用连接蓝牙时出了问题。虽然不是LE ScanFilter,但正如楼主所说,它也是一个权限问题。

根本原因是自SDK 23开始,您需要使用ActivityrequestPermissions()方法在运行时提示用户授予权限。

以下是对我有效的解决方法:

  1. 将下列两行代码之一添加到AndroidManifest.xml文件中的根节点内:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    在尝试连接蓝牙之前,在您的Activity中调用ActivityrequestPermissions()方法,它会打开一个系统对话框来提示用户授权。权限对话框在不同的线程中打开,所以请确保在尝试连接到蓝牙之前等待结果。
    重写ActivityonRequestPermissionsResult()方法以处理结果。如果用户拒绝授予权限,则此方法确实需要做某些事情,以告诉用户应用程序无法执行蓝牙活动。 这篇博客文章有一些使用AlertDialog告诉用户正在发生什么的示例代码。这是一个很好的起点,但有一些缺点:
    • 它没有处理等待requestPermissions()线程完成的操作。
    • 包装requestPermissions()调用的AlertDialog对我来说似乎是多余的。一个裸露的requestPermissions()调用就足够了。

我们能否在不请求“ACCESS_COARSE_LOCATION”权限的情况下获取BLE读数? - IgorGanapolsky

14

在蓝牙权限之外添加位置权限

<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" />
复制粘贴此方法以请求和授予位置权限。
  @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;
    }

然后在onCreate中检查权限

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

希望它能为您节省时间。


2

1
我猜只使用精确定位就可以解决问题,因为它覆盖了粗略定位规格,并具有额外的GPS定位检测范围! - aya salama
是的,主要问题是我们需要两者而不是其中一个,谢谢。 - V-SHY

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接