# 蓝牙权限申请

目标应用的 targetSdkVersion >= 31,那么应该申请一下权限

  • 搜索蓝牙设备权限 :
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
  • 开启蓝牙对其他设备可见的权限:
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
  • 与已经配对过的设备进行通讯:
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
  • 传统蓝牙权限,需要声明权限的 maxSdkVersion 来满足兼容性。
    1
    2
    3
    4
    5
    <!-- Request legacy Bluetooth permissions on older devices. -->  
    <uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
  • 获取蓝牙设备真实位置信息的权限:
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

目标应用的 targetSdkVersion <= 30,那么应该申请一下权限

1
2
3
4
5
6
<uses-permission android:name="android.permission.BLUETOOTH"/>
//if(version > 29){
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
//} else {
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
//}

1
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>

// 发现设备权限

1
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

# 经典蓝牙连接

# 创建 BluetoothAdapter 对象

val adapter = BluetoothAdapter.getDefaultAdapter()

获取蓝牙状态:蓝牙是否开启,如果没有开启,就跳转到蓝牙开启页面

1
2
3
4
if (bluetoothAdapter?.isEnabled == false) {  
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}

监听蓝牙状态变化,蓝牙状态由 4 种:

  • STATE_ON 蓝牙已开启,该状态下才能继续使用 BluetoothAdapter 进行蓝牙配对
  • STATE_OFF 蓝牙已关闭。
  • STATE_TURNING_ON,正在开启种,该状态 BluetoothAdapter 还无法使用
  • STATE_TURNING_OFF 正在关闭种,可以在这个状态去关闭蓝牙连接

使用系统广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1 声明权限 
<uses-permission android:name="android.permission.BLUETOOTH"/>


//2 开启广播监听
fun registerBluetoothReceiver(context : Context){
val intentFilter = IntentFilter().apply {
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
}
context.registerReceiver(bluetoothReceiver,intentFilter)
}

val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when(intent?.action){
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,BluetoothAdapter.STATE_OFF)
val prestate = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE,BluetoothAdapter.STATE_OFF)
}
}
}
}

# 扫描蓝牙设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun registerBluetoothReceiver(context : Context){  
val intentFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
}
context.registerReceiver(bluetoothReceiver,intentFilter)
}


val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when(intent?.action){
BluetoothDevice.ACTION_FOUND -> {
val device: BluetoothDevice? =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
device?.run {
val deviceName = name
val deviceHardwareAddress = address // MAC address
}

}
}
}
}

# 配对

蓝牙建立连接也是一个 C/S 模型,需要一个 Server 和一个 Client

  • BluetoothServerSocket 蓝牙服务端 Socket 接口,监听客户端的连接,收到了连接请求后会返回一个 BluetoothSocket 接口对象用来通讯
  • BluetoothSocket 蓝牙双方通讯的 socket 接口

# 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private inner class AcceptThread : Thread() {  

private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord("YOUR DEVICE NAME", UUID.fromString("MY_UUID"))
}

override fun run() {
// Keep listening until exception occurs or a socket is returned.
var shouldLoop = true
while (shouldLoop) {
val socket: BluetoothSocket? = try {
mmServerSocket?.accept()
} catch (e: IOException) {
Log.e(TAG, "Socket's accept() method failed", e)
shouldLoop = false
null }
socket?.also {
manageMyConnectedSocket(it)
mmServerSocket?.close()
shouldLoop = false
}
}
}

// Closes the connect socket and causes the thread to finish.
fun cancel() {
try {
mmServerSocket?.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the connect socket", e)
}
}
}


fun manageMyConnectedSocket(socket : BluetoothSocket){
//execute bluetooth communicate
}

# 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private inner class ConnectThread(device: BluetoothDevice) : Thread() {  

private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
device.createRfcommSocketToServiceRecord(UUID.fromString("MY_UUID"))
}

public override fun run() {
// Cancel discovery because it otherwise slows down the connection.
bluetoothAdapter?.cancelDiscovery()

mmSocket?.let { socket ->
// Connect to the remote device through the socket. This call blocks
// until it succeeds or throws an exception. socket.connect()

// The connection attempt succeeded. Perform work associated with
// the connection in a separate thread. manageMyConnectedSocket(socket)
}
}

// Closes the client socket and causes the thread to finish.
fun cancel() {
try {
mmSocket?.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the client socket", e)
}
}
}

# 数据传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private val handler: Handler = Handler()  
val MESSAGE_READ: Int = 0
val MESSAGE_WRITE: Int = 1
val MESSAGE_TOAST: Int = 2


private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

private val mmInStream: InputStream = mmSocket.inputStream
private val mmOutStream: OutputStream = mmSocket.outputStream
private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream

override fun run() {
var numBytes: Int // bytes returned from read()

// Keep listening to the InputStream until an exception occurs. while (true) {
// Read from the InputStream.
numBytes = try {
mmInStream.read(mmBuffer)
} catch (e: IOException) {
Log.d(TAG, "Input stream was disconnected", e)
break
}

// Send the obtained bytes to the UI activity.
val readMsg = handler.obtainMessage(
MESSAGE_READ, numBytes, -1,
mmBuffer)
readMsg.sendToTarget()
}
}

// Call this from the main activity to send data to the remote device.
fun write(bytes: ByteArray) {
try {
mmOutStream.write(bytes)
} catch (e: IOException) {
Log.e(TAG, "Error occurred when sending data", e)

// Send a failure message back to the activity.
val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
val bundle = Bundle().apply {
putString("toast", "Couldn't send data to the other device")
}
writeErrorMsg.data = bundle
handler.sendMessage(writeErrorMsg)
return
}

// Share the sent message with the UI activity.
val writtenMsg = handler.obtainMessage(
MESSAGE_WRITE, -1, -1, mmBuffer)
writtenMsg.sendToTarget()
}

// Call this method from the main activity to shut down the connection.
fun cancel() {
try {
mmSocket.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the connect socket", e)
}
}
}

# 低功耗蓝牙(Bluetooth Low Energy)

# BLE 设备发现

https://www.bluetooth.com/blog/advertising-works-part-1/
低功耗蓝牙协议栈中包含的一个用来帮助设备彼此发现并连接的配置文件:Generic Access Profile (GAP)
这个发现过程里面就有一个称之为 "advertise'' 的动作,表明设备想被其他设备发现。
处于 Advertise 的设备会持续向周围发送很小的数据包,让周围的扫描设备能够发现它。
对于 GAP 协议来说 发送 advertise 广播的设备可以被看做是边缘设备;也可以作为一个广播者存在,不接受任何的连接请求,单纯只发送广播。

处于扫描中的设备是作为 GAP 的观察者,当其对连接上其他设备感兴趣时,被作为 GAP 中心设备。

概念:
PDU: packat data unit
ADV: advertising 广播

Android 中低功耗蓝牙发送广播与开启扫描方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//第一步依然是初始化BluetoothAdapter,
val adapter = BluetoothAdapter.getDefaultAdapter()

// next step:

//发送广播,在边缘设备上实现,Peripheral
fun startAdvertising(){
bluetoothAdapter?.bluetoothLeAdvertiser?.run {
val setting = AdvertiseSettings.Builder().apply {
/*设置Advertise广播的参数
* 设置广播模式 三种模式:ADVERTISE_MODE_LOW_POWER 低功耗,默认模式
* ADVERTISE_MODE_BALANCED 平衡模式,平衡了广播频率与电量消耗
* ADVERTISE_MODE_LOW_LATENCY 传统模式,高耗电模式。
*/ setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
// 广播是否是可连接的
setConnectable(true)
//设置广播时长,如果为0,则不会中断
setTimeout(0)
/*
* 设置广播传输功耗等级,可以限制广播数据包的可见范围:
* ADVERTISE_TX_POWER_ULTRA_LOW 最低
* ADVERTISE_TX_POWER_LOW 低
* ADVERTISE_TX_POWER_MEDIUM 中
* ADVERTISE_TX_POWER_HIGH 高
* */ setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW)
}.build()
// Ble广播数据包,需要注意的是AdvertiseData中包含的数据大小不能超过31个字节
val data = AdvertiseData.Builder().apply {
setIncludeDeviceName(true)
setIncludeTxPowerLevel(true)
addServiceUuid(ParcelUuid.fromString(UUID.randomUUID().toString()))
addManufacturerData(0x0001, byteArrayOf())
}.build()
val callback = object :AdvertiseCallback(){
override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
super.onStartSuccess(settingsInEffect)
}

override fun onStartFailure(errorCode: Int) {
super.onStartFailure(errorCode)
}
}
startAdvertising(setting,data,callback)
}
}




//扫描广播,在中心设备上实现,Center
fun discovery(){
bluetoothAdapter?.bluetoothLeScanner?.apply {
val scanSetting = ScanSettings.Builder().apply {
setLegacy(true)
setScanMode(ScanSettings.SCAN_MODE_BALANCED)
}.build()
val filter = mutableListOf<ScanFilter>().apply {
add(ScanFilter.Builder().apply {
setServiceUuid(ParcelUuid.fromString(UUID.randomUUID().toString()))
setManufacturerData(0x0001, byteArrayOf())
}.build())
}.toList()
startScan(filter,scanSetting,object: ScanCallback(){
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
}

override fun onBatchScanResults(results: MutableList<ScanResult>?) {
super.onBatchScanResults(results)
}

override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
}
})

}
}