You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)

问题原因与修复方案

核心原因

  1. 权限检查逻辑无效:当前代码仅在权限未授予时触发申请,但未等待权限申请结果,线程会直接执行cancelDiscovery(),此时权限仍未生效。
  2. Android 12+权限规则:API 31及以上版本中,BLUETOOTH_SCAN、BLUETOOTH_CONNECT属于危险权限,必须明确申请且通过后才能执行蓝牙操作。
  3. 可能遗漏清单权限声明:即使申请了运行时权限,清单文件未声明对应权限也会导致权限不生效。

修复步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 10:49:55