事物的难度远远低于对事物的恐惧
0.前言
最近完成了一个基于BLE蓝牙通信的简单APP,在这里记录下来,供大家参考希望能给需要的人解决疑惑。这个APP中一共是两个界面,第一个界面实现打开蓝牙、关闭蓝牙、扫描蓝牙和显示扫描的结果,通过选择扫描得到的蓝牙设备跳转到第二个界面,再次过程中与选择的蓝牙设备进行连接并获取服务。在第二个界面上显示连接的状态,以及可以发送数据和接收数据,这个代码只是实现了蓝牙传输数据的最基本功能,可以在这个基础上根据实际的需求进行更多的开发!
1.获取权限
首先第一步要获取蓝牙权限和位置权限,在 Androi 6.0(API 23)
以上不仅需要静态添加权限(在 AndroidMainfest.xml
中配置),而且还需要在程序中动态的获取权限,以下是部分需要动态获取的权限。
所属权限组 | 权限 |
---|---|
日历 | READ_CALENDAR |
日历 | WRITE_CALENDAR |
相机 | CAMERA |
联系人 | READ_CONTACTS |
联系人 | WRITE_CONTACTS |
联系人 | GET_CONTACTS |
位置 | ACCESS_FINE_LOCATION |
位置 | ACCESS_COARSE_LOCATION |
麦克风 | RECORD_AUDIO |
电话 | READ_PHONE_STATE |
电话 | CALL_PHONE |
电话 | READ_CALL_LOG |
电话 | WRITE_CALL_LOG |
电话 | ADD_VOICEMAIL |
电话 | USE_SIP |
电话 | PROCESS_OUTGOING_CALLS |
传感器 | BODY_SENSORS |
短信 | SEND_SMS |
短信 | RECEIVE_SMS |
短信 | READ_SMS |
短信 | RECEICE_WAP_PUSH |
短信 | RECEIVE_MMS |
短信 | READ_EXTERNAL_STORAGE |
短信 | WRITE_EXTERNAL_STORAGE |
注意:如果应用程序请求在 AndroidManifest.xml
中列出的危险权限,并且应用程序已经在同一权限组中具有另一个危险权限,系统会立即授予权限,而不会与用户进行任何交互,。例如,如果一个应用程序先前已经请求并被授予 ACCESS_FINE_LOCATION
权限,然后它请求 ACCESS_COARSE_LOCATION
(同属于位置一组),系统会立即授予该权限,不会再弹出权限授予询问的对话框。
在本文中使用的是 ACCESS_FINE_LOCATION
、 ACCESS_COARSE_LOCATION
危险权限和BLUETOOTH
、 BLUETOOTH_ADMIN
普通权限,首先需要在AndroidMainfest.xml
文件中静态添加权限:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
其中BLUETOOTH
是蓝牙的普通权限,如果需要对蓝牙进行操作还需要蓝牙的高级权限BLUETOOTH_ADMIN
,因为这两个权限属于普通权限只需要静态添加即可,剩下两个权限属于危险权限需要动态添加。
2.动态添加权限
首先,先确定是否在AndroidMainfest.xml
文件中静态添加了这个权限,若已经添加可以继续进行动态添加,否则无法进行动态添加。
先进行静态添加(模板)
<uses-permission android:name="android.permission.上表的权限字符" />
然后进行动态添加(模板)
public void requestPower() {
//判断是否已经赋予权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.上表权限字符)
!= PackageManager.PERMISSION_GRANTED) {
//如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.上表权限字符)) {//这里可以写个对话框之类的项向用户解释为什么要申请权限,并在对话框的确认键后续再次申请权限.它在用户选择"不再询问"的情况下返回false
} else {
//申请权限,字符串数组内是一个或多个要申请的权限,1是申请权限结果的返回参数,在onRequestPermissionsResult可以得知申请结果
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.上表权限字符,}, 1);
}
}
}
注意,因为判断语句,其实实际上当用户拒绝一次权限申请后,再次调用方法,不会再出现申请权限对话框。
3.本文使用的动态申请权限方法
先静态添加
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
然后动态添加
public void requestPower() {
//判断是否已经赋予权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
//如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
//这里可以写个对话框之类的项向用户解释为什么要申请权限,并在对话框的确认键后续再次申请权限.它在用户选择"不再询问"的情况下返回false
} else {
//申请权限,字符串数组内是一个或多个要申请的权限,1是申请权限结果的返回参数,在onRequestPermissionsResult可以得知申请结果
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
}
}
}
注意:这里将获取动态权限的方法封装在一个方法里面,需要在 onCreat
方法中进行调用,才能获取权限
4.初始化界面
在第一个界面上有三个按钮(开启蓝牙、关闭蓝牙、扫描蓝牙)和一个Spinner
控件(用来显示扫描到的蓝牙设备),对其进行初始化,并设置相应的点击事件,看代码:
private final static String TAG = "hello";
private Button btn_Search, btn_On, btn_Off;
private Spinner mSpinner;
private BluetoothManager bluetoothManager;
private BluetoothAdapter bluetoothAdapter;
private BluetoothLeScanner bluetoothLeScanner;
//初始化界面
public void initView() {
bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
btn_Search = findViewById(R.id.btn_Search); //扫描蓝牙按钮
btn_On = findViewById(R.id.btn_On); //开启蓝牙按钮
btn_Off = findViewById(R.id.btn_Off); //关闭蓝牙按钮
mSpinner = findViewById(R.id.spinner); //显示蓝牙设备的Spinne控件
btn_Search.setOnClickListener(this);
btn_On.setOnClickListener(this);
btn_Off.setOnClickListener(this);
}
上面的代码完成了第一个界面里面控件的初始化和设置点击事件,其中的部分代码:
private BluetoothManager bluetoothManager;
private BluetoothAdapter bluetoothAdapter;
private BluetoothLeScanner bluetoothLeScanner;
bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
是完成蓝牙的打开、关闭和扫描的操作,在下文将详细的介绍。
5.打开BLE蓝牙
添加完各种权限之后,接下来就需要对BLE蓝牙进行操作,首先是打开APP的时候需要打开蓝牙,先上代码:
private BluetoothManager bluetoothManager;
private BluetoothAdapter bluetoothAdapter;
bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
先定义BluetoothManager
类(蓝牙管理器)和BluetoothAdapter
类(蓝牙适配器)的对象,对象bluetoothManager
通过getSystemService(Context.BLUETOOTH_SERVICE)
方法获取,对象bluetoothAdapter
通过bluetoothManager.getAdapter()
方法获取,如果设备支持蓝牙则bluetoothAdapter
不为null
,否则为null
。接下来就可以开启蓝牙了,上代码
public void turnOnBle() {
if (bluetoothAdapter != null) {
if (bluetoothAdapter.isEnabled() == false) {
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, 11);
} else {
Toast.makeText(this, "蓝牙已经打开了", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "该设备不支持蓝牙", Toast.LENGTH_SHORT).show();
}
}
首先判断bluetoothAdapter
不为null
才能继续往下操作,否则说明该设备不支持蓝牙,然后通过bluetoothAdapter.isEnabled()
方法判断该设备当前蓝牙的状态,若返回的是true
则说明该设备当前蓝牙处于开启的状态,并通过Toast
(弹窗)提示用户,若返回的是false
则说明该设备当前蓝牙处于关闭的状态,然后通过Intent
方法实现开启蓝牙,先创建一个Intent
对象,接着用startActivityForResult(intent, 11)
方法实现intent
的传递,其中第一个参数是Intent
类对象,第二个是一个Code
可以自己定义,后续可以在回调函数中判断蓝牙是否成功的开启,在本文中没有进行这个回调方法的代码编写,感兴趣的可以自己实现一下。
6.关闭蓝牙
当打开蓝牙设备之后,我们还可以通过手动完成关闭蓝牙的操作,先看代码
public void turnOffBle() {
if (bluetoothAdapter.isEnabled()) {
bluetoothAdapter.disable();
} else {
Toast.makeText(this, "蓝牙已经关闭了", Toast.LENGTH_SHORT).show();
}
}
首先通过bluetoothAdapter.isEnabled()
方法得到当前蓝牙的状态,只有当蓝牙处于开启的状态的时候才进行关闭的操作,用bluetoothAdapter.disable()
方法实现蓝牙的关闭,否则提示用户当前蓝牙处于关闭的状态,无需进行关闭的操作。
7.扫描蓝牙
开始搜索代码
完成了蓝牙打开和关闭的操作之后,接下来要是实现获取周围蓝牙设备的操作,即蓝牙的扫描。刚开始本来想使用BluetoothAdapter.startLeScan(BluetoothAdapter.LeScanCallback)
进行扫描,但是发现该方法提示已经过时,不建议使用,后来查阅资料后发现建议使用BluetoothLeScanner.startScan(ScanCallback)
进行扫描,先看代码:
private Handler mHanler = new Handler();
private BluetoothLeScanner bluetoothLeScanner;
bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
//开启蓝牙扫描
public void searchBle() {
if (bluetoothAdapter.isEnabled()) {
mHanler.postDelayed(myRunnable, 5000);
btn_Search.setText("停止搜索");
bluetoothLeScanner.startScan(scanCallback);
} else {
Toast.makeText(MainActivity.this, "请打开蓝牙", Toast.LENGTH_SHORT).show();
}
}
//延时停止搜索线程
Runnable myRunnable = new Runnable() {
@Override
public void run() {
bluetoothLeScanner.stopScan(scanCallback);
btn_Search.setText("搜索设备");
setBLESpinner();
}
};
在开始扫描之前需要得到一个BluetoothLeScanner
类的对象,然后在初始化函数中通过bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner()
进行初始化,然后进行扫描蓝牙的操作。
在开启蓝牙扫描的代码中,同样首先是通过bluetoothAdapter.isEnabled()
判断蓝牙当前的状态,若返回的是false
则说明当前蓝牙没有打开,提示用户请打开蓝牙;若返回的是true则开启蓝牙扫描,首先是mHanler.postDelayed(myRunnable, 5000)
设置一个延时的异步线程,即在5秒后执行bluetoothLeScanner.stopScan(scanCallback)
关闭蓝牙的扫描,其中参数scanCallback
是蓝牙扫描的回调函数,在回调函数中记录扫描得到的设备,在下文将详细的介绍。
然后bluetoothLeScanner.startScan(scanCallback)
开启蓝牙的扫描,并将扫描蓝牙的按钮的文字设置成“停止搜索” btn_Search.setText("停止搜索")
,扫描5秒,时间到了之后停止扫描,并将扫描蓝牙按钮的文字设置成“搜索设备”,这里更改按钮文字的目的是当用户点击了按钮,根据按钮上的文字选择执行不同的函数,如果显示的文字是“搜索设备”则执行searchBle()
函数进行搜索,如果显示的文字是“停止搜索”则执行disSearchBle()
停止搜索,该函数将在下文进行详细的介绍。
在停止搜索的代码中的 setBLESpinner()
函数是更新Spinner
控件中的内容,即显示搜索到的设备。
停止搜索和回调函数代码
private List<BluetoothDevice> listdevices = new ArrayList<BluetoothDevice>(); //存放扫描到的设备
private List<String> listdevicename = new ArrayList<String>(); //存放扫描到的设备名字
//停止搜索
public void disSearchBle() {
if (bluetoothLeScanner != null) {
mHanler.removeCallbacks(myRunnable);
bluetoothLeScanner.stopScan(scanCallback);
btn_Search.setText("搜索设备");
setBLESpinner();
}
}
//扫描蓝牙设备的回调
public ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, final ScanResult result) {
runOnUiThread(new Runnable() {
@Override
public void run() {
BluetoothDevice device = result.getDevice();
String str = device.getName();
if ((listdevicename.indexOf(str) == -1) && (device.getName() != null)) {
listdevices.add(device);
listdevicename.add(str);
Log.i(TAG, str);
}
}
});
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
上面的代码是停止扫描和扫描蓝牙的回调函数,在上文中我们设置了当用户点击了搜索设备的按钮,5秒之后才会停止搜索,这里我们添加了停止搜索的实现函数,在搜索的过程中用户可以点击停止搜索的按钮(和搜索设备是同一个按钮,只是显示的文字不同)停止搜索设备,当点击了这个按钮首先执行mHanler.removeCallbacks(myRunnable)
移除我们之前设置的5秒后执行的线程,然后执行bluetoothLeScanner.stopScan(scanCallback)
停止搜索,执行btn_Search.setText("搜索设备")
将按钮显示的文字设置成“搜索设备”,最后执行setBLESpinner()
更新Spinner
控件的内容。
在扫描蓝牙的回调函数中,当设备正在搜索蓝牙设备的时候,每当搜索到一个设备就会执行一次public void onScanResult(int callbackType, final ScanResult result)
方法,在这个方法中首先得到扫描到的设备并获取该设备的名字,然后判断名字是否为null
,是否已经保存了,如果上述两个条件都没有满足,则将会把该设备存入到listdevicename
集合中,将该设备的名字存入到listdevicename
集合中,这两个集合时全局变量,后续会在setBLESpinner()
方法中使用,实现显示扫描得到的设备。
8.显示扫描得到的蓝牙设备
在本文中是将扫描得到的蓝牙设备放到一个Spinner
控件中进行显示,上文中我们已经得到了周围蓝牙设备的名字和设备,分别存在两个集合中,代码如下:
private Spinner mSpinner;
private ArrayAdapter<String> ListAdapter;
mSpinner = findViewById(R.id.spinner); //显示蓝牙设备的Spinne控件
//将扫描到的设备添加到Spinner控件中
public void setBLESpinner() {
mSpinner.setOnItemSelectedListener(this);
ListAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, listdevicename);
ListAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSpinner.setAdapter(ListAdapter);
}
首先为Spinner
控件中的每一项设置选中监听,选中其中的某一项的时候会将这个蓝牙设备的名字和地址通过Intent
传递给第二个界面,从而实现连接、传输数据等,这个将在下文进行详细的说明。然后将listdevicename
中存放的蓝牙设备名字放入Spinner
控件中进行显示,但是不能直接放入,中间需要一个ArrayAdapter
类的对象进行过渡一下才能正常的显示。
9.选择需要连接的蓝牙设备
完成显示之后,我们就可以选择我们需要连接的蓝牙设备,先看代码:
public static String EXTRAS_DEVICE_NAME = "DEVICE_NAME";
public static String EXTRAS_DEVICE_ADDRESS = "DEVICE_ADDRESS";
//选择蓝牙设备
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String Item = parent.getSelectedItem().toString();
if (Item != "<BLE List>") {
for (BluetoothDevice device : listdevices) {
if (Item.equals(device.getName())) {
Intent intent = new Intent(this, BLECommunicationActivity.class);
intent.putExtra(EXTRAS_DEVICE_NAME, device.getName());
intent.putExtra(EXTRAS_DEVICE_ADDRESS, device.getAddress());
startActivity(intent);
}
}
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
在上文的mSpinner.setOnItemSelectedListener(this)
我们设置了选中监听事件,当我们选择一个蓝牙设备的时候就执行上面的代码,首先获得我们选中的Item
即蓝牙设备的名字,如果不是我们添加的字符串“<BLE List>”
,然后开始遍历我们之前保存的扫描到的蓝牙设备的集合,根据名字找到对应的蓝牙设备,然后将该设备的名字和地址传递给第二个界面,并跳转到第二个界面中,进行后续的操作。
10.按钮点击监听事件
下面是按钮的点击监听事件的代码:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_On: //打开蓝牙按钮
turnOnBle();
break;
case R.id.btn_Off: //关闭蓝牙按钮
turnOffBle();
break;
case R.id.btn_Search: //搜索蓝牙设备按钮
if (btn_Search.getText().equals("搜索设备")) {
listdevices.clear();
listdevicename.clear();
listdevicename.add("<BLE List>");
searchBle();
} else if (btn_Search.getText().equals("停止搜索")) {
disSearchBle();
}
break;
}
}
当点击搜索蓝牙设备按钮,会判断按钮显示的文字,根据不同的文字执行相应的方法,其中
listdevices.clear();
istdevicename.clear();
listdevicename.add("<BLE List>");
这两行代码作用是在每一次搜索设备之前需要将存放蓝牙设备和名字两个集合清空,存放新扫描到的设备和名字。由于Spinner
控件会自动选中第一条的内容,为了防止发生误连接的现象,我们使用listdevicename.add("<BLE List>")
这条语句确保每一次扫描结束之后存放蓝牙设备名字的集合第一条是我们自己定义的字符串,然后在public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
方法中判断选中的是否是我们自定义的字符串,如果不是才进行跳转和连接。
这是第一个界面上代码,只实现了一部分的功能,接下来的功能将在 Android BLE蓝牙配置全流程(二) 附APP源码 中进行说明,如果需要请前往查看。如有任何问题可以留言或者私信我,看到会及时回复,上述的内容如果有误,还请各位大佬不吝赐教,在此,祝愿各位学习顺利,前途无限~
Android BLE蓝牙(HC-08)源码免费获取方式