欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android蓝牙4.0扫描

程序员文章站 2024-03-24 23:10:16
...

本文主要记录一下Android扫描蓝牙设备的方法。


  1. 初始化蓝牙和注册广播:
    private void initBluetooth() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mBluetoothAdapter != null) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(BluetoothDevice.ACTION_FOUND);
            filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
            filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
            mContext.registerReceiver(mBluetoothReceiver, filter);
        }
    }

首先获取BluetoothAdapter蓝牙适配器,通过适配器可以扫描蓝牙设备,就是BluetoothDevice对象。如果使用广播来获取扫描结果的话,还需要注册蓝牙扫描广播(ACTIONFOUND)、蓝牙开始扫描(ACTION_DISCOVERY_STARTED)和完成扫描的广播(ACTIN_DISCOVERY_FINISHED)。另外一种方法是使用LeScanCallback回调获取结果,这种放在Android API中提示在API 21之后弃用,但是经过我测试,在5.1,7.0上仍然可以使用,5.0上效果仍然很好,但是在7.0上效率会很差,经常搜索不到设备。
2. 开始扫描蓝牙设备和取消扫描:

    /**
     * 开始扫描蓝牙设备
     */
    private void startScan() {
        if (mBluetoothAdapter != null) {
            mBluetoothAdapter.startDiscovery();
        }
    }

    /**
     * 取消扫描
     */
    private void cancelScan() {
        if (mBluetoothAdapter != null) {
            mBluetoothAdapter.cancelDiscovery();
        }
    }

扫描的开始和取消都很简单,分别调用蓝牙适配器的startDiscovery()和cancelDiscovery()函数即可。
3. 最后创建一个广播接收器,来接受扫描结果:

    private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int rssi = intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);
                addDevice(device, rssi);
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
                sendDevices();
            }
        }
    };

    /**
     * 添加设备
     */
    private void addDevice(BluetoothDevice device, int rssi) {
        BLeDevice bLeDevice = new BLeDevice(device.getName(), device.getAddress(), rssi);
        Iterator<BLeDevice> iterator = mDeviceList.iterator();
        while (iterator.hasNext()) {
            if (device.getAddress().equals(iterator.next().getAddress()))
                iterator.remove();  //如果多次扫描到同一台设备,则移除之前的设备
        }
        mDeviceList.add(bLeDevice);
    }

    /**
     * 将扫描到的设备发送到前台
     */
    private void sendDevices() {
        Message msg = new Message();
        msg.obj = mDeviceList;
        mHandler.sendMessage(msg);
    }

注册了广播后,每搜索到一台设备,系统就会发送一次ACTION_FOUND广播,所以我们可以通过intent.getAction()来筛选出这个广播,然后通过intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)取出序列化对象BluetoothDevice。因为蓝牙定位需要用到rssi,所以我还获取了rssi值,获取的方法是intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI)。之后将获取到的蓝牙名称,地址和RSSI赋给创建的实体类BLeDevice,再把BLeDevice方到ArrayList集合中。在扫描完成的广播中,我们将扫描到的设备通过handler发送到前台,可以用来在页面显示。
4. 最后别忘了添加蓝牙权限,还有定位权限,6.0之后定位权限需要主动申请:

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

.
5. 因为蓝牙定位需要周期扫描设备,所以我使用了Timer定时器,本来是用AlarmManager的,但是它在Android7.0之后最小周期必须在5s以上,最终只能把它抛弃了。另外除了使用广播之外还有另外两种方法,分别是在API 21弃用的LeScallback和新增的android.bluetooth.le包下的BluetoothLeScanner和Scallback类。我全部都封装到了BeaconScanner类中。

BeaconScanner类完整的代码:

import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Handler;
import android.os.Message;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;

public class BeaconScanner {

    private Context mContext;
    private Handler mHandler;

    private Timer mTimer;
    private Handler mHandlerTimer;
    private static final int MIN_DELAY = 600;
    private static final int MIN_PERIOD = 2000;

    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothLeScanner mBluetoothLeScanner;
    private ArrayList<BeaconDevice> mDeviceList = new ArrayList<>();

    private boolean isRegisterReceiver = false;

    public BeaconScanner(Context mContext, Handler mHandler) {
        this.mContext = mContext;
        this.mHandler = mHandler;
        this.mHandlerTimer = new Handler();
        this.mTimer = new Timer();
        initBluetooth2();
    }

    /**
     * 周期扫描设备
     *
     * @param period 扫描周期,单位毫秒
     */
    public void start(long period) {
        if (period <= 0) {
            period = MIN_PERIOD;
        }
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                startScan();
            }
        }, 0, period);
    }

    public void close() {
        if(isRegisterReceiver){
            mContext.unregisterReceiver(mBluetoothReceiver);
            isRegisterReceiver = false;
        }
        if (mTimer != null) {
            mTimer.cancel();
        }
    }

    private void sendDevices() {
        Collections.sort(mDeviceList, new SignalComparator());
        Message msg = new Message();
        msg.obj = mDeviceList;
        mHandler.sendMessage(msg);
    }

    // 添加设备
    private void addDevice(BluetoothDevice device, int rssi) {
        BeaconDevice beaconDevice = new BeaconDevice(device.getName(), device.getAddress(), rssi);
        Iterator<BeaconDevice> iterator = mDeviceList.iterator();
        while (iterator.hasNext()) {
            if (device.getAddress().equals(iterator.next().getAddress()))
                iterator.remove();  //如果多次扫描到同一台设备,则移除之前的设备
        }
        mDeviceList.add(beaconDevice);
    }

    //===================注册广播接受扫描结果=========================================================
    /**
     * 初始化蓝牙适配器,注册蓝牙搜索,开始搜索蓝牙,完成搜索3个广播
     */
    private void initBluetooth() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mBluetoothAdapter != null) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(BluetoothDevice.ACTION_FOUND);
            filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
            filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
            mContext.registerReceiver(mBluetoothReceiver, filter);
            isRegisterReceiver = true;
        }
    }


    /**
     * 开始扫描蓝牙设备
     */
    private void startDiscovery() {
        mDeviceList.clear(); // 每一次扫描之前要清除上一次扫描的设备
        if (mBluetoothAdapter != null) {
            mBluetoothAdapter.startDiscovery();
        }
        mHandlerTimer.postDelayed(new Runnable() {
            @Override
            public void run() {
                cancelDiscovery();
            }
        }, MIN_DELAY);
    }

    /**
     * 取消扫描,并将扫到的设备发送到前台
     */
    private void cancelDiscovery() {
        if (mBluetoothAdapter != null) {
            mBluetoothAdapter.cancelDiscovery();
        }
    }

    private BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int rssi = intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);
                addDevice(device, rssi);
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
                sendDevices();
            }
        }
    };
    //===================注册广播接受扫描结果=========================================================

    //===================使用LeScanCallback接受扫描结果===============================================
    private void initBluetooth2(){
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    private void startScan(){
        mDeviceList.clear();
        if (mBluetoothAdapter != null) {
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        }
        mHandlerTimer.postDelayed(new Runnable() {
            @Override
            public void run() {
                stopScan();
            }
        }, MIN_DELAY);
    }

    private void stopScan(){
        if (mBluetoothAdapter != null)
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        sendDevices();
    }

    private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            addDevice(device, rssi);
        }
    };

    //===================使用LeScanCallback接受扫描结果===============================================

    //===================使用ScanCallback接受扫描结果=================================================

    /**
     * Android5.0(API = 21)调用
     * BluetoothLeScanner此类提供了执行蓝牙LE设备扫描相关操作的方法。应用程序可以使用扫描特定类型的蓝牙LE
     * 设备ScanFilter。它也可以请求不同类型的回调来传递结果;
     * <p>
     * 在Android7.0(Mi 5s,E350)上测试效果很差,可以说是间接性抽风,时好时坏。
     * 在Android5.1(G21)上测试效果一般,相比7.0好很多,搜索效率在50-60%
     * 在Android4.4(X3)上报NoClassDefFoundError异常,报错位置是创建ScanCallback匿名内部类。未解决。猜测
     * 是因为不支持蓝牙4.0,或者Android版本过低。
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void initBluetoothLe(){
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mBluetoothLeScanner =  mBluetoothAdapter.getBluetoothLeScanner();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void startScanLe(){
        mDeviceList.clear();
        if (mBluetoothLeScanner != null) {
            mBluetoothLeScanner.startScan(mScanCallback);
        }
        mHandlerTimer.postDelayed(new Runnable() {
            @Override
            public void run() {
                stopScanLe();
            }
        }, MIN_DELAY);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void stopScanLe(){
        if (mBluetoothLeScanner != null) {
            mBluetoothLeScanner.stopScan(mScanCallback);
        }
        sendDevices();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            BluetoothDevice device = result.getDevice();
            int rssi = result.getRssi();
            addDevice(device, rssi);
        }
    };
    //===================使用ScanCallback接受扫描结果=================================================

    /**
     * 根据信号强度降序排列
     */
    class SignalComparator implements Comparator<BeaconDevice> {

        @TargetApi(Build.VERSION_CODES.KITKAT)
        @Override
        public int compare(BeaconDevice o1, BeaconDevice o2) {
            return Integer.compare(o2.getSignal(), o1.getSignal());
        }
    }
}

注意:使用BeaconScanner类:在Activity或Service中调用start()开始周期扫描蓝牙,close()解除注册广播和关闭timer定时器。如果需要更换方法需要在BeaconScanner手动更改,首先在构造函数中更换初始化方法,然后在start()中更换对应的开始扫描方法。
BeaconDevice 实体类:

import android.os.Parcel;
import android.os.Parcelable;

public class BeaconDevice implements Parcelable {

    private String name;
    private String address;
    private int signal;

    public BeaconDevice(String name, String address, int signal) {
        this.name = name;
        this.address = address;
        this.signal = signal;
    }

    protected BeaconDevice(Parcel in) {
        name = in.readString();
        address = in.readString();
        signal = in.readInt();
    }

    public static final Parcelable.Creator<BeaconDevice> CREATOR = new Parcelable.Creator<BeaconDevice>() {
        @Override
        public BeaconDevice createFromParcel(Parcel in) {
            return new BeaconDevice(in);
        }

        @Override
        public BeaconDevice[] newArray(int size) {
            return new BeaconDevice[size];
        }
    };

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getSignal() {
        return signal;
    }

    public void setSignal(int signal) {
        this.signal = signal;
    }

    @Override
    public String toString() {
        return "BeaconDevice{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", signal=" + signal +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        // 序列化
        dest.writeString(name);
        dest.writeString(address);
        dest.writeInt(signal);
    }
}

使用的话在Activity或Service中,调用beaconScan.start()函数,传入一个扫描周期时间即可,别忘了在程序结束的时候调用beaconScan.close()取消广播注册和关闭闹钟。
总结
1.注册广播的方法:推荐,经过测试在Android4.4,5.1,7.0上搜索效率都很好,而且稳定。
2.使用LeScanCallback回调:虽说Android API提示在API 21 后弃用,但是经过测试在7.0上仍然可以使用,只是效果很差,而在Android4.4和5.1上面效果比第一种方法效率更高。如果不适配高版本,可以使用这个方法。
3.使用ScanCallback回调:不推荐,效果很差,首先在Android4.4上面直接崩溃,运行到ScanCallback内部类创建时报NoClassDefFoundError,猜测是因为我的测试机不支持蓝牙4.0。另外不管在Android5.1和7.0上,搜索效果都很差,只能说是间接性抽风,时好时坏。
因为测试次数有限,可能受机器和Android版本的影响,所以不保证测试的准确性,具体还要自己测试
文章示例源码下载:Android蓝牙扫描