Android蓝牙BLE连接ESP32时BLUETOOTH_SCAN权限异常排查
问题:Android蓝牙连接ESP32时BLUETOOTH_SCAN权限异常
开发一款通过BLE模式连接ESP32蓝牙设备的Android应用,已在首次启动时申请运行时权限,但执行MyBtEngine.java代码时仍抛出java.lang.SecurityException,提示缺少android.permission.BLUETOOTH_SCAN权限。异常出现在调用BluetoothAdapter.cancelDiscovery()的第135行,确认权限已申请但问题依旧。
MyBtEngine.java代码
package com.example.myapplication; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.util.Log; import androidx.core.app.ActivityCompat; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; public class MyBtEngine { static final int BT_STATE_NONE = 0; static final int BT_STATE_CONNECTING = 1; static final int BT_STATE_CONNECTED = 2; //uid for all 3rd party devices (not android) private static final UUID UUID_BT_DEVICE = UUID.fromString("55072829-bc9e-4c53-938a-74a6d4c78776"); private static final String BT_DEVICE_MAC = "78:E3:6D:09:63:38";//put your device MAC !!!! private Context cont; private BtWaitConnThread mWaitConnThread = null; private BtWorkThread mWorkThread = null; private final BluetoothAdapter mAdapter; private int mState; MyBtEngine(Context context) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mState = BT_STATE_NONE; cont = context; } synchronized void start() throws IOException { if (mAdapter == null) { return; } BluetoothDevice device; try { device = mAdapter.getRemoteDevice(BT_DEVICE_MAC); } catch (Exception e) { return; } if (mWaitConnThread != null) { mWaitConnThread.cancel(); mWaitConnThread = null; } if (mWorkThread != null) { mWorkThread.cancel(); mWorkThread = null; } mWaitConnThread = new BtWaitConnThread(device); mWaitConnThread.start(); setState(BT_STATE_CONNECTING); } public synchronized void stop() { if (mWaitConnThread != null) { mWaitConnThread.cancel(); mWaitConnThread = null; } if (mWorkThread != null) { mWorkThread.cancel(); mWorkThread = null; } setState(BT_STATE_NONE); } private synchronized void setState(int state) { Log.e("MyBtEngine", "setState() " + mState + " -> " + state); mState = state; } synchronized int getState() { return mState; } public boolean writeBt(byte[] out) { BtWorkThread r; // temp obj , just to keep write function not to destroyed if mWorkThread finish synchronized (this) { if ((mWorkThread == null) || (mState != BT_STATE_CONNECTED)) { return false; } r = mWorkThread; } r.write(out); return true; } private synchronized void startWorker(BluetoothSocket socket, BluetoothDevice device) { if (mWaitConnThread != null) { mWaitConnThread.cancel(); mWaitConnThread = null; } if (mWorkThread != null) { mWorkThread.cancel(); mWorkThread = null; } mWorkThread = new BtWorkThread(socket); mWorkThread.start(); setState(BT_STATE_CONNECTED); } private class BtWaitConnThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; private BtWaitConnThread(BluetoothDevice device) throws IOException { mmDevice = device; BluetoothSocket tmp = null; if (ActivityCompat.checkSelfPermission(cont, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { new PermissionCheck().checkPermission(cont); } } tmp = device.createRfcommSocketToServiceRecord(UUID_BT_DEVICE); mmSocket = tmp; } public void run() { if (ActivityCompat.checkSelfPermission(cont, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { new PermissionCheck().checkPermission(cont); } } mAdapter.cancelDiscovery(); try { mmSocket.connect(); } catch (IOException e) { try { mmSocket.close(); } catch (IOException e2) { } setState(BT_STATE_NONE); return; } synchronized (MyBtEngine.this) { mWaitConnThread = null;//set itself to null because thread exit soon } startWorker(mmSocket, mmDevice); } private void cancel() { try { mmSocket.close(); } catch (IOException e) { } } } private class BtWorkThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; private BtWorkThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { while (true) { try { int charFromEsp = mmInStream.read();//TODO: add code here for handling input from ESP } catch (IOException e) { try { MyBtEngine.this.start();//restart from beginning } catch (IOException ex) { ex.printStackTrace(); } return; // exit worker thread } } } public boolean write(byte[] buffer) { try { mmOutStream.write(buffer); } catch (IOException e) { return false; } return true; } private void cancel() { try { mmSocket.close(); } catch (IOException e) { } } } public int returnState(){ return mState; } }
报错信息
java.lang.SecurityException: Need android.permission.BLUETOOTH_SCAN permission for android.content.AttributionSource@efdade4f: AdapterService cancelDiscovery at android.os.Parcel.createExceptionOrNull(Parcel.java:2456) at android.os.Parcel.createException(Parcel.java:2440) at android.os.Parcel.readException(Parcel.java:2423) at android.os.Parcel.readException(Parcel.java:2365) at android.bluetooth.IBluetooth$Stub$Proxy.cancelDiscovery(IBluetooth.java:3247) at android.bluetooth.BluetoothAdapter.cancelDiscovery(BluetoothAdapter.java:1874) at com.example.myapplication.MyBtEngine$BtWaitConnThread.run(MyBtEngine.java:135)
问题原因与修复方案
核心原因
- 权限检查逻辑无效:当前代码仅在权限未授予时触发申请,但未等待权限申请结果,线程会直接执行
cancelDiscovery(),此时权限仍未生效。 - Android 12+权限规则:API 31及以上版本中,
BLUETOOTH_SCAN、BLUETOOTH_CONNECT属于危险权限,必须明确申请且通过后才能执行蓝牙操作。 - 可能遗漏清单权限声明:即使申请了运行时权限,清单文件未声明对应权限也会导致权限不生效。
修复步骤
1. 完善清单文件权限声明
在AndroidManifest.xml中添加以下权限:
<!-- 适配Android 11及以下蓝牙权限 --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> <!-- Android 12+蓝牙权限 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <!-- 部分设备扫描BLE需要位置权限(适配Android 11及以下) --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/> <!-- 声明支持BLE --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
2. 修改子线程中的权限检查逻辑
子线程不能直接处理权限申请,需确保权限已授予后再执行蓝牙操作:
public void run() { // 检查Android 12+的BLUETOOTH_SCAN权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ActivityCompat.checkSelfPermission(cont, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { Log.e("MyBtEngine", "BLUETOOTH_SCAN权限未授予,终止连接"); setState(BT_STATE_NONE); return; } } else { // Android 11及以下检查位置权限 if (ActivityCompat.checkSelfPermission(cont, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { Log.e("MyBtEngine", "位置权限未授予,终止连接"); setState(BT_STATE_NONE); return; } } // 权限验证通过,执行后续操作 if (mAdapter.isDiscovering()) { mAdapter.cancelDiscovery(); } try { mmSocket.connect(); } catch (IOException e) { try { mmSocket.close(); } catch (IOException e2) { } setState(BT_STATE_NONE); return; } synchronized (MyBtEngine.this) { mWaitConnThread = null; } startWorker(mmSocket, mmDevice); }
3. 在主线程中处理权限申请
权限申请必须在主线程发起,可在Activity中实现:
private static final int REQUEST_BT_PERMISSIONS = 1001; private MyBtEngine myBtEngine; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myBtEngine = new MyBtEngine(this); // 启动前先申请权限 requestBtPermissions(); } private void requestBtPermissions() { String[] permissions; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissions = new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}; } else { permissions = new String[]{Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION}; } ActivityCompat.requestPermissions(this, permissions, REQUEST_BT_PERMISSIONS); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_BT_PERMISSIONS) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { // 权限全部通过,启动蓝牙连接 try { myBtEngine.start(); } catch (IOException e) { e.printStackTrace(); } } else { Toast.makeText(this, "蓝牙权限未授予,无法连接设备", Toast.LENGTH_SHORT).show(); } } }
额外优化点
- 若仅连接已知MAC的设备,可在
BLUETOOTH_SCAN权限中添加android:usesPermissionFlags="neverForLocation",避免申请位置权限。 - 调用
cancelDiscovery()前先判断mAdapter.isDiscovering(),减少不必要的系统调用。
内容的提问来源于stack exchange,提问作者SeikoFPS
相关产品推荐
相关产品推荐

