Android蓝牙4.0扫描
本文主要记录一下Android扫描蓝牙设备的方法。
- 初始化蓝牙和注册广播:
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蓝牙扫描
下一篇: JavaScript代码的运行机制