Android经典蓝牙开发全流程

article/2025/10/15 15:17:29

一、基本介绍

  所谓蓝牙(Bluetooth)技术,实际上是一种短距离无线电技术,最初是由爱立信公司公司发明的。技术始于爱立信公司 1994 方案,它是研究在移动电话和其他配件间进行低功耗、低成本无线通信连接的方法。发明者希望为设备间的通讯创造一组统一规则(标准化协议)用来解决用户间相互不兼容的移动电子设备。
  1998年5月20日,索尼以立信、国际商业机器、英特尔、诺基亚及东芝公司等业界龙头创立“特别兴趣小组”(Special Interest Group SIG),即蓝牙技术联盟的前身,目标是开发一个成本低、效益高、可以在短距离范围内随意无线连接的蓝牙技术标准,是负责蓝牙规范的制定和推广的国际组织。

在这里插入图片描述

  蓝牙发展至今经历了多个版本的更新,1.1、1.2、2.0、2.1、3.0、4.0、4.1、4.2、5.0等。其中,将1.x~3.0之间的版本称之为经典蓝牙,4.x开始的蓝牙称之为低功耗蓝牙,也就是蓝牙ble。根据应用、协议类型等,可以对蓝牙进行以下分类:

在这里插入图片描述


二、经典蓝牙API介绍

  Android平台包含蓝牙网络堆栈支持,此支持能让设备以无线方式与其他蓝牙设备交换数据。应用框架提供通过Android Bluetooth API访问蓝牙功能的权限。这些API允许应用以无线方式连接到其他蓝牙设备,从而实现点到点和多点无线功能。Android应用可通过Bluetooth API执行以下操作:

  • 扫描其他蓝牙设备
  • 查询本地蓝牙适配器的配对蓝牙设备
  • 建立 RFCOMM 通道
  • 通过服务发现连接到其他设备
  • 与其他设备进行双向数据传输
  • 管理多个连接

以下对经典蓝牙开发相关的API进行介绍:

1、BluetoothAdapter类

  BluetoothAdapter代表了移动设备的本地的蓝牙适配器, 通过该蓝牙适配器可以对蓝牙进行基本操作, 例如 : 启动设备发现,获取已配对设备,通过mac蓝牙地址获取蓝牙设备等。

(1)获取本地蓝牙适配器实例

方法定义:

/*** 作用:*	获取本地蓝牙适配器实例* 参数:*	无* 返回:*	如果设备具备蓝牙功能,返回BluetoothAdapter 实例;否则,返回null对象。*/
public static synchronized BluetoothAdapter getDefaultAdapter();

使用说明:

1、获取默认本地蓝牙适配器的句柄。目前Android仅支持一个蓝牙适配器,但该API可以扩展为支持更多。

(2)打开蓝牙

方法定义:

/*** 作用:*	打开蓝牙* 参数:*	无* 返回:*	如果蓝牙开始打开,则返回true;如果蓝牙打开发生问题,则返回false。*/
public boolean enable();

使用说明:

1、需要 BLUETOOTH_ADMIN权限。
2、该方法将不经过用户同意,直接启用底层蓝牙硬件,并启动所有蓝牙系统服务。由于不同Android设备系统的实现不同,所以部分Android系统在调用该方法时也会弹框请求用户同意。
3、打开蓝牙,还可以通过调用startActivityForResult方法,使用ACTION_REQUEST_ENABLE意图来实现,此方法将弹出对话框,请求允许打开蓝牙。可以在Activity中的onActivityResult()方法中处理操作结果。
4、该方法是一个异步调用:它将立即返回结果。如果此调用返回true,则适配器状态将立即从STATE_OFF转换为STATE_TURNING_ON,并且稍后过渡到STATE_OFF或STATE_ON 。如果此调用返回false,则说明出现问题阻止适配器开启,例如设备处于飞行模式,或者蓝牙已打开。因此还应当监听ACTION_STATE_CHANGED广播,以跟踪后续蓝牙状态更改。

(3)关闭蓝牙

方法定义:

/*** 作用:*	关闭蓝牙* 参数:*	无* 返回:*	如果蓝牙开始关闭,则返回true;如果蓝牙关闭发生问题,则返回false。*/
public boolean disable();

使用说明:

1、需要 BLUETOOTH_ADMIN权限。
2、该方法将不经过用户同意,关闭所有蓝牙连接,停止蓝牙系统服务并关闭底层蓝牙硬件。由于不同Android设备系统的实现不同,所以部分Android系统在调用该方法时也会弹框请求用户同意。
3、该方法是一个异步调用:它将立即返回结果。如果此调用返回true,则适配器状态将立即从STATE_ON转换为STATE_TURNING_OFF,并且稍后过渡到STATE_OFF或STATE_ON 。如果此调用返回false,则说明出现问题阻止适配器关闭,例如适配器已关闭。因此还应当监听ACTION_STATE_CHANGED广播,以跟踪后续蓝牙状态更改。

(4)验证蓝牙MAC地址

方法定义:

/*** 作用:*	验证蓝牙设备MAC地址是否有效。* 参数:*	address:蓝牙MAC地址,字母必须大写,例如:"00:43:A8:23:10:F1"。* 返回:*	如果蓝牙MAC地址有效,则返回true;否则返回false。*/
public static boolean checkBluetoothAddress(String address);

使用说明:

(5)获取本地蓝牙MAC地址

方法定义:

/*** 作用:*	获取本地蓝牙适配器的硬件地址(MAC地址)* 参数:*	无* 返回:*	本地的硬件地址,例如:"00:11:22:AA:BB:CC"。*/
public String getAddress();

使用说明:

1、需要 BLUETOOTH权限。

(6)获取蓝牙绑定列表

方法定义:

/*** 作用:*	获取与本机蓝牙所有绑定的远程蓝牙信息。* 参数:*	无* 返回:*	将本地蓝牙适配器绑定的一组BluetoothDevice对象返回。若出现错误返回null。*/
public Set<BluetoothDevice> getBondedDevices();

使用说明:

1、需要 BLUETOOTH权限。
2、若蓝牙未打开,将返回空集。

(7)获取蓝牙名称

方法定义:

/*** 作用:*	获取本地蓝牙适配器的蓝牙名称。* 参数:*	无* 返回:*	本地蓝牙名称。若出现错误,返回null。*/
public String getName();

使用说明:

1、需要 BLUETOOTH权限。

(8)设置蓝牙名称

方法定义:

/*** 作用:*	设置本地蓝牙适配器的蓝牙名称。* 参数:*	name:蓝牙名称。* 返回:*	设置成功返回true,否则返回false。*/
public boolean setName(String name);

使用说明:

1、需要 BLUETOOTH_ADMIN权限。
2、如果蓝牙未打开,该方法将返回false。
3、尽管许多远程设备只能显示前40个字符,而有些可能仅限于20个,但有效的蓝牙名称最多使用UTF-8编码为248个字节。

(9)获取蓝牙扫描模式

方法定义:

/*** 作用:*	获取本地蓝牙适配器的当前蓝牙扫描模式。* 参数:*	无* 返回:*	当前蓝牙适配器的蓝牙扫描模式。*/
public int getScanMode();

使用说明:

1、需要 BLUETOOTH权限。
2、蓝牙扫描模式确定本地蓝牙适配器是否可被远程蓝牙设备连接和发现。
3、如果蓝牙未打开,此方法将返回SCAN_MODE_NONE。

蓝牙扫描模式:

名称值(int)含义
SCAN_MODE_NONE20该设备不能扫描以及被扫描。
SCAN_MODE_CONNECTABLE21该设备可以扫描其他蓝牙设备。
SCAN_MODE_CONNECTABLE_DISCOVERABLE23该设备既可以扫描其他设备,也可以被其他设备扫描发现。

(10)获取蓝牙适配器状态

方法定义:

/*** 作用:*	获取本地蓝牙适配器的当前状态。* 参数:*	无* 返回:*	当前蓝牙适配器状态。*/
public int getState();

使用说明:

1、需要 BLUETOOTH权限。

蓝牙适配器状态:

名称值(int)含义
STATE_OFF10表示本地蓝牙适配器已关闭
STATE_TURNING_ON11表示本地蓝牙适配器正在打开
STATE_ON12表示本地蓝牙适配器已开启,并可供使用
STATE_TURNING_OFF13表示本地蓝牙适配器正在关闭

(11)蓝牙是否打开

方法定义:

/*** 作用:*	判断当前蓝牙适配器是否打开* 参数:*	无* 返回:*	若蓝牙为打开状态,则返回true,否则返回false。*/
public boolean isEnabled();

使用说明:

1、需要 BLUETOOTH权限。
2、如果蓝牙正处于打开状态并可用,则返回true值,getState()==STATE_ON 等价。

(12)蓝牙是否正在扫描

方法定义:

/*** 作用:*	判断蓝牙适配器是否正在处于扫描过程中。* 参数:*	无* 返回:*	若蓝牙处于扫描状态,则返回true;否则返回false。*/
public boolean isDiscovering();

使用说明:

1、需要 BLUETOOTH权限。
2、若蓝牙未打开,该方法将返回false。
3、扫描设备是一个重量级过程,不应在扫描时尝试建立连接,而此时已存在的蓝牙连接将获得有限制的带宽以及高延迟。

(13)启动扫描

方法定义:

/*** 作用:*	开始扫描周边蓝牙设备。* 参数:*	无* 返回:*	若启动成功,返回true;否则返回false。*/
public boolean startDiscovery();

使用说明:

1、需要 BLUETOOTH_ADMIN权限。
2、通常为12秒左右的查询扫描过程。
3、这是一个异步调用,它会立即返回。注册ACTION_DISCOVERY_STARTED和ACTION_DISCOVERY_FINISHED广播以确定发现何时开始和完成的确切时间。注册ACTION_FOUND以便在发现远程蓝牙设备时收到通知。
4、若蓝牙未打开,该方法将返回false。
5、扫描设备是一个重量级过程,不应在扫描时尝试建立连接,而此时已存在的蓝牙连接将获得有限制的带宽以及高延迟。可以使用cancelDiscovery()取消扫描操作。

(14)取消扫描

方法定义:

/*** 作用:*	取消蓝牙搜索操作* 参数:*	无* 返回:*	如果取消成功, 则返回true; 如果取消失败, 返回false。*/
public boolean cancelDiscovery()

1、需要 BLUETOOTH_ADMIN权限。
2、若蓝牙未打开,该方法将返回false。
3、因为蓝牙搜索是一个重量级过程,会耗费大量资源,所以在连接远程蓝牙设备前,必须调用这个方法,取消搜索。

(15)获取远程蓝牙设备

方法定义:

/*** 作用:*	获取给定蓝牙硬件地址的BluetoothDevice对象。* 参数:*	address:蓝牙MAC地址,字母必须大写,例如:"00:43:A8:23:10:F1"。* 返回:*	指定的远程蓝牙设备。*/
public BluetoothDevice getRemoteDevice(String address);

使用说明:

1、如果MAC无效无效,将抛出IllegalArgumentException异常。

(16)创建不安全的蓝牙服务套接字

方法定义:

/*** 作用:*	创建一个正在监听的不安全的带有服务记录的无线射频通信(RFCOMM)蓝牙端口。* 参数:*	name:SDP记录下的服务器名,可以是任意字符串。*	uuid:SDP记录下的UUID。* 返回:*	BluetoothServerSocket对象。*/
public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid);

使用说明:

1、需要 BLUETOOTH权限。
2、系统将分配一个未使用的RFCOMM通道进行侦听。
3、当发生错误时,例如蓝牙不可用、权限不足、通道被占用等,将抛出IOException异常。
4、通过此方式创建的蓝牙服务套接字是不安全的,连接时不需要进行配对。

(17)创建安全的蓝牙服务套接字

方法定义:

/*** 作用:*	创建一个正在监听的安全的带有服务记录的无线射频通信(RFCOMM)蓝牙端口。* 参数:*	name:SDP记录下的服务器名,可以是任意字符串。*	uuid:SDP记录下的UUID。* 返回:*	BluetoothServerSocket对象。*/
public BluetoothServerSocket listenUsingRfcommWithServiceRecord(String name, UUID uuid);

使用说明:

1、需要 BLUETOOTH权限。
2、系统将分配一个未使用的RFCOMM通道进行侦听。
3、当发生错误时,例如蓝牙不可用、权限不足、通道被占用等,将抛出IOException异常。
4、通过此方式创建的蓝牙服务套接字是安全的,连接时需要进行配对。


2、BluetoothDevice类

  BluetoothDevice对象代表了一个远程的蓝牙设备, 通过这个类可以查询远程设备的物理地址, 名称, 连接状态等信息。这个类实际上只是一个蓝牙硬件地址的简单包装,这个类的对象是不可变的。对这个类的操作, 会执行在远程蓝牙设备的硬件上。

(1)获取蓝牙名称

方法定义:

/*** 作用:*	获取远程蓝牙设备的蓝牙名称。* 参数:*	无* 返回:*	成功则返回蓝牙名称,若出现问题则返回null。*/
public String getName();

使用说明:

1、需要BLUETOOTH权限。
2、执行设备扫描时,本地适配器将自动检索远程名称,并将缓存它们。此方法仅从缓存中返回此设备的名称。

(2)获取蓝牙MAC

方法定义:

/*** 作用:*	获取远程蓝牙设备的硬件地址* 参数:*	无* 返回:*	蓝牙硬件地址*/
public String getAddress();

使用说明:

(3)获取蓝牙绑定状态

方法定义:

/*** 作用:*	获取远程蓝牙设备的绑定状态* 参数:*	无* 返回:*	蓝牙绑定状态*/
public int getBondState();

使用说明:

1、需要BLUETOOTH权限。

蓝牙绑定状态:

名称值(int)含义
BOND_NONE10远程设备未绑定。
BOND_BONDING11正在与远程设备进行绑定。
BOND_BONDED12远程设备已绑定。

(4)绑定远程设备

方法定义:

/*** 作用:*	开始与远程蓝牙设备的绑定过程。* 参数:*	无* 返回:*	若成功开始绑定则返回true,否则返回false。*/
public boolean createBond();

使用说明:

1、需要BLUETOOTH_ADMIN权限。
2、这是一个异步调用,它会立即返回。注册监听ACTION_BOND_STATE_CHANGED广播,当绑定过程完成时及时获取其结果通知。
3、Android系统服务将处理必要的用户交互以确认并完成绑定过程。

(5)创建不安全的蓝牙套接字

方法定义:

/*** 作用:*	创建不安全的蓝牙套接字。* 参数:*	uuid:用于查找RFCOMM通道的服务记录UUID* 返回:*	一个准备好外界连接的RFCOMM蓝牙服务端口*/
public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid);

使用说明:

1、需要BLUETOOTH权限。
2、通信渠道将不会有认证的链接密钥,即它将受到中间人攻击。
3、对于蓝牙 2.1 设备,链接将被加密,因为加密是强制性的。对于旧设备(蓝牙 2.1 之前的设备),链接不会被加密。
4、它旨在与listenUsingInsecureRfcommWithServiceRecord(String, UUID)用于对等蓝牙应用。
5、出现错误时,例如蓝牙不可用、权限不足,将抛出IOException异常。

(6)创建安全的蓝牙套接字

方法定义:

/*** 作用:*	创建安全的蓝牙套接字。* 参数:*	uuid:用于查找RFCOMM通道的服务记录UUID* 返回:*	一个准备好外界连接的RFCOMM蓝牙服务端口*/
public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid);

使用说明:

1、需要BLUETOOTH权限。
2、只有经过身份验证的套接字链接才可以使用此套接字。认证是指认证链路密钥,以防止中间人攻击。
3、此套接字上的通信将被加密。
4、这是为与对等蓝牙应用程序 listenUsingRfcommWithServiceRecord(String, UUID)配合使用而设计的。
5、出现错误时,例如蓝牙不可用、权限不足,将抛出IOException异常。


3、BluetoothServerSocket类

  BluetoothServerSocket是一个侦听蓝牙服务套接字。使用BluetoothServerSocket可以创建一个监听服务端口, 使用accept方法阻塞, 当该方法监测到连接的时候, 就会返回一个BluetoothSocket对象来管理这个连接。BluetoothServerSocket是线程安全的,close方法始终会立即中止正在进行的操作并关闭蓝牙服务套接字。需要BLUETOOTH权限。

(1)阻塞等待连接(无超时)

方法定义:

/*** 作用:*	阻塞直到建立连接。* 参数:*	无* 返回:*	成功连接时连接的BluetoothSocket对象。*/
public BluetoothSocket accept();

使用说明:

1、一旦这个调用返回,它可以被再次调用来接受后续的传入连接。
2、close()可用于从另一个线程中止此调用。
3、发生错误时,例如该调用被中止、超时,将抛出IOException异常。

(2)阻塞等待连接(有超时)

方法定义:

/*** 作用:*	阻塞直到建立连接或超时。* 参数:*	timeout:阻塞等待的超时时间。* 返回:*	成功连接时连接的BluetoothSocket对象。*/
public BluetoothSocket accept(int timeout);

使用说明:

1、一旦这个调用返回,它可以被再次调用来接受后续的传入连接。
2、close()可用于从另一个线程中止此调用。
3、发生错误时,例如该调用被中止、超时,将抛出IOException异常。

(3)关闭

方法定义:

/*** 作用:*	关闭该监听服务端口,并释放所有关联的资源。* 参数:*	无* 返回:*	无*/
public void close();

使用说明:

1、该方法将导致其他线程在此套接字上阻塞的调用立即引发IOException异常。
2、关闭这个端口不会关闭accept()方法返回的BluetoothSocket对象。
3、该方法调用出现问题,将抛出IOException异常。


4、BluetoothSocket类

  BluetoothSocket是蓝牙套接口。在服务器端,使用BluetoothServerSocket创建侦听服务器套接字。当连接被BluetoothServerSocket接受时,它将返回一个新的BluetoothSocket来管理连接。 在客户端,使用单个BluetoothSocket来启动连接并管理连接。最常见的蓝牙套接字类型是RFCOMM,它是Android API支持的类型。 RFCOMM是一种通过蓝牙实现的面向连接的流媒体传输。它也被称为串口行为规范(SPP)。BluetoothSocket是线程安全的,close方法始终会立即中止正在进行的操作并关闭套接字。需要BLUETOOTH权限。

(1)连接

方法定义:

/*** 作用:*	尝试连接到远程蓝牙服务器。* 参数:*	无* 返回:*	无*/
public void connect();

使用说明:

1、此方法将阻塞,直到建立连接或连接失败。 如果此方法没有异常返回,则此套接字现在已连接。
2、该方法调用出现问题时,例如连接失败,将抛出IOException异常。

(2)是否连接

方法定义:

/*** 作用:*	获取此套接字的连接状态,即是否与远程蓝牙服务连接。* 参数:*	无* 返回:*	若连接则返回true,否则返回false。*/
public boolean isConnected();

使用说明:

(3)获取远程蓝牙设备

方法定义:

/*** 作用:*	获取此套接字连接的远程蓝牙设备。* 参数:*	无* 返回:*	连接的远程蓝牙设备BluetoothDevice对象。*/
public BluetoothDevice getRemoteDevice();

使用说明:

(4)获取输入流

方法定义:

/*** 作用:*	获取与此套接字关联的输入流。* 参数:*	无* 返回:*	输入流对象。*/
public InputStream getInputStream();

使用说明:

1、即使套接字尚未连接,输入流也会返回,但对该流的操作将抛出IOException异常,直到关联的套接字连接。
2、该方法调用出错时,将抛出IOException异常。
3、通过此方法获取的输入流对象,可以读取对端发送的数据。

(5)获取输出流

方法定义:

/*** 作用:*	获取与此套接字关联的输出流。* 参数:*	无* 返回:*	输出流对象。*/
public OutputStream getOutputStream();

使用说明:

1、即使套接字尚未连接,输出流也会返回,但对该流的操作将抛出IOException异常,直到关联的套接字连接。
2、该方法调用出错时,将抛出IOException异常。
3、通过此方法获取的输出流对象,可以发送数据给对端。

(6)关闭

方法定义:

/*** 作用:*	关闭此流并释放与其关联的所有系统资源。如果流已经关闭,则调用此方法不起作用。* 参数:*	无* 返回:*	无*/
public void close();

使用说明:

1、该方法调用出现问题,将抛出IOException异常。


三、经典蓝牙开发流程

1、经典蓝牙开发流程分析

在这里插入图片描述

2、蓝牙服务端实现

(1)在工程清单文件AndroidManifest.xml中添加权限:
<!--如果使用了BLUETOOTH_ADMIN权限,那么必须使用BLUETOOTH权限-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/><!--android6.0后需要搜索周边蓝牙设备,需要添加以下两个权限-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/><!--要求设备硬件必须支持蓝牙-->
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
(2)获取本地蓝牙适配器:
BluetoothAdapter mAdapter= BluetoothAdapter.getDefaultAdapter();
(3)打开蓝牙:
//方式一:通过Intent来向用户弹框请求打开蓝牙,可以重写onActivityResult来监听打开蓝牙的请求结果
//打开蓝牙
public void openBluetooth(){if(mBluetoothAdapter==null){//自定义方法,用来往TextView上添加提示信息showTip("当前设备不支持蓝牙功能!");return;}if(mBluetoothAdapter.isEnabled()){showTip("蓝牙已打开");return;}Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);startActivityForResult(intent,GlobalDef.REQ_CODE_OPEN_BT);
}@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);if(requestCode==GlobalDef.REQ_CODE_OPEN_BT){if(resultCode == Activity.RESULT_OK){showTip("蓝牙打开成功");}else{showTip("蓝牙打开失败");}}
}//方式二:通过enable方法静默打开蓝牙,无需用户同意(部分Android系统使用该方法依然会弹框提示,向用户请求打开蓝牙)
mBluetoothAdapter.enable();
(4)关闭蓝牙
//关闭蓝牙,无需用户同意(部分Android系统使用该方法依然会弹框提示)
mBluetoothAdapter.disable();
(5)允许蓝牙可见:
//方式一:通过Intent方式向用户请求允许蓝牙被搜索
//注:如果蓝牙没有开启,用户点击确定后,会首先开启蓝牙,继而设置蓝牙能被扫描
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//设置蓝牙可见性的时间,默认持续时间为120秒,每个请求的最长持续时间上限为300秒
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120);
startActivity(intent);//方式二:通过反射的方式来设置蓝牙可见性,且不会出现弹框
//注:如果蓝牙没有开启,通过此方式并不会直接打开蓝牙
/*** 设置蓝牙可见* @param adapter* @param timeout 超时为0时,永久可见*/
public static void setDiscoverableTimeout(BluetoothAdapter adapter, int timeout) {//BluetoothAdapter adapter=BluetoothAdapter.getDefaultAdapter();try {Method setDiscoverableTimeout = BluetoothAdapter.class.getMethod("setDiscoverableTimeout", int.class);setDiscoverableTimeout.setAccessible(true);Method setScanMode =BluetoothAdapter.class.getMethod("setScanMode", int.class,int.class);setScanMode.setAccessible(true);setDiscoverableTimeout.invoke(adapter, timeout);setScanMode.invoke(adapter, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,timeout);} catch (Exception e) {e.printStackTrace();}
}/*** 关闭蓝牙可见* @param adapter*/
public static void closeDiscoverableTimeout(BluetoothAdapter adapter) {try {Method setDiscoverableTimeout = BluetoothAdapter.class.getMethod("setDiscoverableTimeout", int.class);setDiscoverableTimeout.setAccessible(true);Method setScanMode =BluetoothAdapter.class.getMethod("setScanMode", int.class,int.class);setScanMode.setAccessible(true);setDiscoverableTimeout.invoke(adapter, 1);setScanMode.invoke(adapter, BluetoothAdapter.SCAN_MODE_CONNECTABLE,1);} catch (Exception e) {e.printStackTrace();}
}
(6)创建蓝牙服务套接字,等待其他蓝牙客户端连接:
try{mSocketList=new LinkedList<BluetoothSocket>();//用来管理连接的蓝牙套接字mExecutorService= Executors.newCachedThreadPool();//创建线程池//创建蓝牙服务端mServerSocket=mBluetoothAdapter.listenUsingRfcommWithServiceRecord("BluetoothTool", GlobalDef.BT_UUID);mServerRunningFlag=true;showTip("蓝牙服务端成功启动");new Thread(){@Overridepublic void run(){try{BluetoothSocket socket=null;//循环等待蓝牙socket连接while(mServerRunningFlag){socket=mServerSocket.accept();//阻塞式mSocketList.add(socket);//SocketThread为自定义的线程类,用于管理BluetoothSocket的读写操作mExecutorService.execute(new SocketThread(socket));}}catch (Exception e){e.printStackTrace();}}}.start();
}catch(IOException e){e.printStackTrace();showTip("服务端启动出现异常");Log.e(TAG,"runServer IOException");
}
(7)连接成功之后,就通过获取BluetoothSocket的输入输出流进行数据传输:
// 获取流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 写出、读入
byte[] temp=new byte[1024];
inputStream.read(temp);//当无数据时将阻塞等待
outputStream.write(temp);
(8)以下为用于操作BluetoothSocket的SocketThread的简单实现,仅供参考:
class SocketThread extends Thread {private BluetoothSocket mSocket=null;private InputStream mIn;private OutputStream mOut;private boolean isOpen = false;public SocketThread(BluetoothSocket socket) {try {mSocket=socket;mIn = mSocket.getInputStream();mOut = mSocket.getOutputStream();isOpen = true;Log.d(TAG, "a socket thread create");} catch (IOException e) {e.printStackTrace();Log.e(TAG, "create SocketThread fail");}}@Overridepublic void run() {int readLen=0;byte[] buffer=new byte[1024];try{while(isOpen){readLen=mIn.read(buffer);if(readLen>0){Log.i(TAG,"read data length="+readLen);Log.i(TAG,"read data hex = "+StringUtil.bytesToHexString(buffer,0,readLen));}}}catch (Exception e){e.printStackTrace();release();}}/*** 写入数据* @param data* @param offset* @param len*/public void writeData(byte[] data,int offset,int len){if (data == null || offset<0 || len<=0 || (len+offset)>data.length) {Log.e(TAG,"BT writeData params fail");return;}try {mOut.write(data,offset,len);mOut.flush();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public void release(){Log.d(TAG,"A socketThread release");try{isOpen=false;if(mOut!=null){try{mOut.close();}catch (Exception e){e.printStackTrace();}mOut=null;}if(mIn!=null){try{mIn.close();}catch (Exception e){e.printStackTrace();}mIn=null;}if(mSocket!=null){try{mSocket.close();}catch (Exception e){e.printStackTrace();}mSocket=null;}}catch (Exception e){e.printStackTrace();}}
}

3、蓝牙客户端实现

(1)添加权限,同蓝牙服务端。
(2)获取本地蓝牙适配器,同蓝牙服务端。
(3)打开蓝牙,同蓝牙服务端。
(4)关闭蓝牙,同蓝牙服务端。
(5)允许蓝牙可见,同蓝牙服务端。
(6)定义蓝牙广播接收器,用于接收蓝牙搜索、连接状态改变等的广播:
class BluetoothBroadcastReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent){String action=intent.getAction();Log.d(TAG,"Action received is "+action);//蓝牙搜索if(BluetoothDevice.ACTION_FOUND.equals(action)){BluetoothDevice scanDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);if(scanDevice == null || scanDevice.getName() == null){return;}int btType=scanDevice.getType();if(btType==BluetoothDevice.DEVICE_TYPE_LE || btType==BluetoothDevice.DEVICE_TYPE_UNKNOWN){return;}Log.d(TAG, "bt name="+scanDevice.getName()+" address="+scanDevice.getAddress());//将搜索到的蓝牙设备加入列表deviceList.add(scanDevice);short rssi=intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);rssiList.add(rssi);listAdapter.notifyDataSetChanged();//通知ListView适配器更新}//蓝牙配对else if(BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)){BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);if(mCurDevice!=null && btDevice.getAddress().equals(mCurDevice.getAddress())){int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);if(state==BluetoothDevice.BOND_NONE){showTip("已取消与设备" + btDevice.getName() + "的配对");mFlag=-1;}else if(state==BluetoothDevice.BOND_BONDED){showTip("与设备" + btDevice.getName() + "配对成功");mFlag=1;}}}else if(BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)){int blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);switch (blueState) {case BluetoothAdapter.STATE_TURNING_ON:Log.i(TAG,"onReceive---------STATE_TURNING_ON");break;case BluetoothAdapter.STATE_ON:Log.i(TAG,"onReceive---------STATE_ON");showTip("蓝牙当前状态:ON");break;case BluetoothAdapter.STATE_TURNING_OFF:Log.i(TAG,"onReceive---------STATE_TURNING_OFF");break;case BluetoothAdapter.STATE_OFF:Log.i(TAG,"onReceive---------STATE_OFF");showTip("蓝牙当前状态:OFF");break;}}}
}
(7)注册广播:
    mBluetoothBroadcastReceiver=new BluetoothBroadcastReceiver();IntentFilter filter=new IntentFilter();filter.addAction(BluetoothDevice.ACTION_FOUND);filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);mContext.registerReceiver(mBluetoothBroadcastReceiver,filter);
(8)搜索周边蓝牙设备:
    if(mBluetoothAdapter.isDiscovering()){mBluetoothAdapter.cancelDiscovery();}//搜索到的蓝牙设备通过广播接收mBluetoothAdapter.startDiscovery();
(9)建立与蓝牙服务器的连接:
/*** 蓝牙配对并连接*/
public void bondAndConnect(BluetoothDevice mCurDevice){//取消搜索if(mBluetoothAdapter.isDiscovering()){mBluetoothAdapter.cancelDiscovery();}if(mCurDevice==null){showTip("远程蓝牙设备为空!");return;}//当前蓝牙设备未配对,则先进行配对if(mCurDevice.getBondState()==BluetoothDevice.BOND_NONE){Log.d(TAG,"create bond to "+mCurDevice.getName());boolean nRet= BluetoothUtil.createBond(mCurDevice);if(!nRet){showTip("createBond fail!");return;}showLoadingDialog("正在与【"+mCurDevice.getName()+"】进行配对...");mFlag=0;while(mFlag==0){SystemClock.sleep(250);}if(mFlag==-1){showTip("与【"+mCurDevice.getName()+"】的蓝牙配对失败");dismissLoadingDialog();return;}}if(mCurDevice.getBondState()==BluetoothDevice.BOND_BONDED){showLoadingDialog("正在与【"+mCurDevice.getName()+"】进行连接...");try {//创建SocketBluetoothSocket socket = mCurDevice.createRfcommSocketToServiceRecord(GlobalDef.BT_UUID);//连接蓝牙服务套接字socket.connect();mThread=new SocketThread(socket);mThread.start();showTip(("成功与【"+mCurDevice.getName()+"】建立连接"));} catch (IOException e) {Log.d(TAG,"socket connect fail");showTip(("连接【"+mCurDevice.getName()+"】失败"));e.printStackTrace();}}dismissLoadingDialog();
}
(10)连接成功之后,就通过输入输出流进行数据传输,同蓝牙服务端。

四、注意事项与常见问题

1、Android6.0以后,搜索蓝牙设备需要位置权限(危险权限,需要动态申请)。
2、高版本Android系统上进行蓝牙搜索时,除了动态申请位置权限外,有的可能还需要手动打开设备的位置信息,否则无法搜索蓝牙。

在这里插入图片描述

3、搜索周边蓝牙设备时,本机并不需要处于蓝牙可见状态。但其他设备必须处于蓝牙可见状态,本机才可以搜索到。已知蓝牙设备(处于有效范围内)的MAC地址,则随时可以向其发起连接,而无需打开蓝牙可见性。
4、如果尚未在设备上启用蓝牙,则启用设备可检测性将会自动启用蓝牙。
5、搜索周边设备对于蓝牙适配器而言是一个非常繁重的操作过程,并且会消耗大量资源。在找到要连接的设备后,确保始终使用 cancelDiscovery() 停止发现,然后再尝试连接。 此外,如果已经保持与某台设备的连接,那么执行搜索操作可能会大幅减少可用于该连接的带宽,因此不应该在处于连接状态时执行搜索周边蓝牙的操作。
6、在调用 connect() 时,应始终确保设备未在进行搜索操作。 如果正在进行搜索操作,则会大幅降低连接尝试的速度,并增加连接失败的可能性。
7、搜索周边设备,通过广播能够获取到周边蓝牙设备的名称、mac地址、rssi信号强度等。
8、BluetoothSocket是线程安全的, close()方法会终止BluetoothSocket进行的一切操作, 并且同时会关闭连接。
9、经典蓝牙是通过流来进行数据收发的,对流进行数据读取操作时,由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据,因而容易产生数据粘包问题。对于这种问题,可以给每个完整的数据包添加一个起始符与结束符,那么接收方就可以确定需要读取并处理的数据范围。



具体的演示demo放在Github上,感兴趣的同学可以了解一下。如果有什么问题,欢迎来一起讨论,共同进步。

GitHub地址:https://github.com/JINSHENGCCC/Android_Common/tree/master/Android-BT/src


五、附录

  1. 深入了解Android蓝牙Bluetooth——《基础篇》
  2. Android 经典蓝牙开发
  3. Android蓝牙开发—经典蓝牙详细开发流程
  4. 蓝牙概览

http://chatgpt.dhexx.cn/article/mkj55IhJ.shtml

相关文章

Android - 蓝牙开发

文章目录 科普SIG类型制式选择逻辑链路控制适配协议 (L2CAP)L2CAP的功能 蓝牙框架和 RFCOMM 协议蓝牙安全白名单机制 编程蓝牙权限Classic BluetoothBluetooth Low Energy术语角色 & 职能查找 BLE 设备连接设备上的 GATT 服务器绑定服务蓝牙设置连接到设备连接到 GATT 服务…

Android 蓝牙开发 uuid,Android蓝牙开发之 UUID

UUID&#xff1a;全球唯一标识符 在蓝牙中&#xff0c;每个Service和Characteristic都唯一地由"全球唯一标识符" (UUID)来校验&#xff0c;主要是保证他们的唯一性。 UUID可分为&#xff1a;16位、32位、128 位UUID Bluetooth_Base_UUID&#xff1a;蓝牙UUID基数 UUI…

Android 低功耗蓝牙开发简述

低功耗蓝牙简述 一、什么是低功耗蓝牙&#xff1f;二、怎么做低功耗蓝牙应用&#xff1f;① 之前有没有接触Android蓝牙开发&#xff1f;② 蓝牙设备固件是公司自己的吗&#xff1f;③ 有没有蓝牙固件和蓝牙应用的文档和Demo&#xff1f;④ 具体的业务功能需求明确吗&#xff1…

Android蓝牙开发

题引&#xff1a; 最近项目上涉及与硬件相关的功能&#xff0c;需要通过蓝牙进行消息收发。项目已完成&#xff0c;这里做下记录。 通信步骤&#xff1a; 1.初始化BluetoothAdapter.getDefaultAdapter()获取BluetoothAdapter对象 2.判断蓝牙是否开启bluetoothAdapter.isEnab…

【Android】蓝牙开发——BLE(低功耗蓝牙)(附完整Demo)

目录 目录 前言 一、相关概念介绍 二、实战开发 三、项目演示 四、Demo案例源码地址 五、更新记录 1、2020/12/29 &#xff1a;修改 setupService()中错误 2、2021/05/14 &#xff1a;更新连接方法&#xff08;解决部分蓝牙设备连接失败的问题&#xff09; 3、2022/1…

【Bluetooth开发】蓝牙开发入门

BLE 蓝牙设备在生活中无处不在&#xff0c;但是我们也只是将其作为蓝牙模块进行使用&#xff0c;发送简单的AT命令实现数据收发。 那么&#xff0c;像对于一些复杂的使用场合&#xff1a;“车载蓝牙”、"智能手表"、“蓝牙音箱”等&#xff0c;我们不得不去了解底层…

【Bluetooth蓝牙开发】一、蓝牙开发入门

一、蓝牙开发入门 文章目录 一、蓝牙开发入门 1、蓝牙概念2、蓝牙发展历程3、蓝牙技术概述 3.1 Basic Rate(BR)3.2 Low Energy&#xff08;LE&#xff09; 4、常见蓝牙架构 4.1 SOC蓝牙单芯片方案4.2 SOC蓝牙MCU方案4.3 蓝牙host controller分开方案4.4 使用场景 5、参考文档 …

vs2012做ArcGIS二次开发前期准备

解压ArcGIS 1.双击ESRI 2.点击 一路next&#xff0c;自己选择安装路径&#xff0c;建议放在非系统盘 3.开始菜单-ArcGIS-License Server Administrator 4.点击“stop/停止"&#xff0c;再点击确定 5.将破解文件中的两个文件拷到D:\Program Files (x86)\ArcGIS\License10.…

arcgis 二次开发学习笔记(一):了解二次开发有关的软件及其之间的关系

【废话篇】今天是大三开学的第一天课&#xff0c;终于意识到我口中念念不忘却没付出实际行动的“考研”来了。考研目标现在为止还没有很明确&#xff0c;只是不甘屈于人后。周围太多生活得很辛苦的人&#xff0c;只是不愿意我这一辈子也为了有关qian的小事斤斤计较&#xff0c;…

Arcobjects for java:Arcgis二次开发入门,开发一个基本地图组件

一、目的 因学习需要&#xff0c;使用Java进行Arcgis二次开发。当前对arcgis进行二次开发使用的语言基本是C#&#xff0c;使用Java对Arcgis进行二次开发的很少。于是使用java在idea上进行Arcgis二次开发&#xff0c;给入门的同学做参考&#xff0c;我自己也处于入门阶段&#…

ArcGIS二次开发基础教程(00):基础界面设计

ArcGIS二次开发基础教程(00) : 基础界面设计 (开发环境&#xff1a;VS2010ArcEngine10.2C# &#xff1b;鉴于学习ArcGIS二次开发的同学都有一定的WinForm开发和ArcGIS软件使用基础&#xff0c;故此教程不再对一些基础内容作详细阐述&#xff09; 首先新建一个Windows窗体应用程…

【ArcGIS二次开发】TOCControl右键菜单功能实现

1、添加现有项 ①右击解决方案中的项目&#xff0c;添加TOCControlContextMenu中的LayerSelectable、LayerVisibility、RemoveLayer、ZoomToLayer ②点击菜单栏中的项目&#xff0c;添加引用ESRI.ArcGIS.ADF.Local ③修改RemoveLayer中的命名空间为项目名称EngineMapTest&#…

arcgis python实例_arcgis二次开发_arcgis二次开发python_arcgis二次开发实例

[1.rar] - QQ连连看的源码.单消秒杀挂机等功能喜欢的朋友请拿去研究 [qqCHAR.rar] - qq 验证码识别程序 可以叫准确的识别出qq登陆前的验证码 [1.rar] - 本书以Visualc作为开发语言&#xff0c;结合大量实例&#xff0c;详细介绍了利用Arcobjects组件进行GIS二次开发的方法和…

【ArcGIS二次开发】Engine界面搭建

1、新建窗体项目Windows Appplication(Engine) 2、添加menuStrip、statusStrip和ToolbarControl控件&#xff0c;并设置相应的Dock属性为Top和Right 3、用SplitContainer控件把显示区域分成三部分&#xff0c;并设置splitContatiner1的Orientation属性为Horizontal 4、添加TabC…

ArcGIS二次开发基础教程(02):地图导航和鹰眼

ArcGIS二次开发基础教程(02)&#xff1a;地图导航和鹰眼 地图导航&#xff08;主要是调用命令和工具&#xff09; 地图的放缩和漫游 if(axMapControl1.CurrentTool null) {ICommand icc;//地图放大ITool tool new ControlsMapZoomInToolClass();//地图缩小//ITool tool n…

arcgis java 二次开发_arcgis二次开发_cad二次开发_java arcgis二次开发

属性查询是GIS应用不可缺少的重要功能&#xff0c;尤其是在各种业务系统中&#xff0c;根据用户输入相应的查询条件&#xff0c;从属性要素中快速定位到用户感兴趣的要素&#xff0c;为业务应用提供了便利。本文就来聊一聊QGis二次开发中如何实现属性查询功能。 其实这个功能我…

AE+ArcGIS二次开发课程设计(基于C#)

AEArcGIS二次开发课程设计&#xff08;基于C#&#xff09; 1.工作内容2.程序功能介绍3.功能模块介绍3.1 实现【创建TIN】说明3.1.1 功能说明3.1.2 代码实现&#xff08;包含了所有主要的代码&#xff0c;库引用自行导入&#xff09; 3.2 实现【TIN坡度坡向分析】说明3.2.1 功能…

ArcGIS二次开发前言

ArcGIS二次开发前言 前言环境常见bug解决方案 前言 自毕业成为GIS开发工程师已有一年多的时间&#xff0c;时间很短&#xff0c;短到不过人一生中工作时限的3.75%&#xff0c;时间很长&#xff0c;长到收藏夹已经从零攒到了一千四百多条记录&#xff0c;OneNote上也记录了几十…

ArcGIS Engine二次开发

目录 1 前言 2 准备工作 2.1 License的加入 2.2 ToolStrip控件 2.3 MenuStrip控件 2.4 帮助文档的查看 3 数据加载 3.1 矢量数据的加载 3.2 栅格数据的加载 4 地图浏览功能 1 前言 这是一份关于ArcGIS Engine二次开发的一份报告总结&#xff0c;在这份报告中包含了简单的…

ArcGIS二次开发知识点总结

空间分析定义&#xff1a;空间分析是指分析具有空间坐标或相对位置的数据和过程的理论和方法&#xff0c;是对地理空间现象的定量研究&#xff0c;其目的在于提取并传输空间数据中隐含的空间信息。 叠置分析定义&#xff1a;是指将同一坐标系统下不同信息表达的两组或多组专题…