Bluetooth LE 扫描在后台失败 - 权限
Bluetooth LE Scan fails in the background - permissions
以下代码在我的 Nexus 9 运行 Android 5.1.1(Build LMY48M)上运行良好,但在 Nexus 9 运行 Android 6.0(构建 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() {
...
});
在 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 上工作吗?
问题不是扫描过滤器,而是后台权限。
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();
}
}
}
}
当您提示用户位置权限时,OS 对话框会为他们提供将该权限请求降级为“仅在使用应用程序时允许”与“始终允许”的选项。如果用户选择第一个选项,您将不会在后台进行检测,即使上面的所有其他设置都已设置。
在 Android 11 上,事情变得更加复杂,因为 OS 为权限请求提供了“仅这一次”的另一个选项。如果您的应用程序以 SDK 30 (Android 11) 为目标,它甚至不会为用户提供“始终允许”的选项,用户将不得不转到“设置”作为一个单独的步骤来打开所有时间访问。有关此在 Android 11.
上的工作方式的更多详细信息,请参阅 here
有关权限提示演变的更广泛讨论,请参阅我的博客 post here。
之前 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
我在连接蓝牙的应用程序中遇到了类似的问题。不是 LE ScanFilter,但这是一个权限问题,就像 OP 一样。
根本原因是从 SDK 23 开始,您需要使用 Activity
的 requestPermissions()
方法在运行时提示用户权限。
这对我有用:
将以下两行之一添加到 AndroidManifest.xml
,在根节点内:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
在您的 Activity 中,在尝试连接到蓝牙之前,调用 Activity
的 requestPermissions()
方法,这会打开一个系统对话框提示用户输入许可。权限对话框在不同的线程中打开,所以在尝试连接蓝牙之前一定要等待结果。
覆盖 Activity
的 onRequestPermissionsResult()
来处理结果。如果用户拒绝授予权限,此方法实际上只需要做一些事情,告诉用户该应用无法进行蓝牙 activity.
This blog post 有一些示例代码使用 AlertDialogs 告诉用户发生了什么。这是一个很好的起点,但也有一些缺点:
- 它不处理等待
requestPermissions()
线程完成
- 包含对
requestPermissions()
的调用的 AlertDialog 对我来说似乎无关紧要。只调用 requestPermissions()
就足够了。
添加位置权限以及 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" />
复制粘贴此方法以请求和授予位置权限
@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 fuckMarshMallow() {
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
fuckMarshMallow();
}
希望它能节省您的时间。
如果您的应用针对 Android Q,仅粗略定位是不够的,您需要使用精细定位,否则会出现此错误:
E/BluetoothUtils: Permission denial: Need ACCESS_FINE_LOCATION permission to get scan results
请参阅 https://developer.android.com/preview/privacy/camera-connectivity#fine-location-telephony-wifi-bt 了解官方来源。
以下代码在我的 Nexus 9 运行 Android 5.1.1(Build LMY48M)上运行良好,但在 Nexus 9 运行 Android 6.0(构建 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() {
...
});
在 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 上工作吗?
问题不是扫描过滤器,而是后台权限。
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();
}
}
}
}
当您提示用户位置权限时,OS 对话框会为他们提供将该权限请求降级为“仅在使用应用程序时允许”与“始终允许”的选项。如果用户选择第一个选项,您将不会在后台进行检测,即使上面的所有其他设置都已设置。
在 Android 11 上,事情变得更加复杂,因为 OS 为权限请求提供了“仅这一次”的另一个选项。如果您的应用程序以 SDK 30 (Android 11) 为目标,它甚至不会为用户提供“始终允许”的选项,用户将不得不转到“设置”作为一个单独的步骤来打开所有时间访问。有关此在 Android 11.
上的工作方式的更多详细信息,请参阅 here有关权限提示演变的更广泛讨论,请参阅我的博客 post here。
之前 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
我在连接蓝牙的应用程序中遇到了类似的问题。不是 LE ScanFilter,但这是一个权限问题,就像 OP 一样。
根本原因是从 SDK 23 开始,您需要使用 Activity
的 requestPermissions()
方法在运行时提示用户权限。
这对我有用:
将以下两行之一添加到
AndroidManifest.xml
,在根节点内:<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
在您的 Activity 中,在尝试连接到蓝牙之前,调用
Activity
的requestPermissions()
方法,这会打开一个系统对话框提示用户输入许可。权限对话框在不同的线程中打开,所以在尝试连接蓝牙之前一定要等待结果。覆盖
Activity
的onRequestPermissionsResult()
来处理结果。如果用户拒绝授予权限,此方法实际上只需要做一些事情,告诉用户该应用无法进行蓝牙 activity.
This blog post 有一些示例代码使用 AlertDialogs 告诉用户发生了什么。这是一个很好的起点,但也有一些缺点:
- 它不处理等待
requestPermissions()
线程完成 - 包含对
requestPermissions()
的调用的 AlertDialog 对我来说似乎无关紧要。只调用requestPermissions()
就足够了。
添加位置权限以及 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" />
复制粘贴此方法以请求和授予位置权限
@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 fuckMarshMallow() {
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
fuckMarshMallow();
}
希望它能节省您的时间。
如果您的应用针对 Android Q,仅粗略定位是不够的,您需要使用精细定位,否则会出现此错误:
E/BluetoothUtils: Permission denial: Need ACCESS_FINE_LOCATION permission to get scan results
请参阅 https://developer.android.com/preview/privacy/camera-connectivity#fine-location-telephony-wifi-bt 了解官方来源。