最近有一个需求,要求在Android APP中,通过蓝牙FTP协议实现文件接收功能。为此,找了许多资料,发现比较简单的实现方案是采用Bluecove库通过OBEX协议来实现。
Bluecove库中已经实现了OBEX协议的解析,同时会调用Android系统的 BluetoothServerSocket与 BluetoothSocket 进行蓝牙通信监听与数据通信。所以利用该库,直接调用相关接口即可,使用比较简单。
Bluecove库下载
-
使用implementation
在Android Studio中可使用 implementation 的方式自动下载bluecove.jar
在build.gradle中加入(当前最新版本是2.1.0)
implementation net.sf.bluecove:bluecove:2.1.0 -
网上下载jar包
从https://mvnrepository.com/artifact/net.sf.bluecove/bluecove/2.1.0可下载最新的jar包,点击下图红圈标记处直接下载,这一方式实则与方式1是同一源

-
直接到github上下载源码
https://github.com/fallowu/bluecove
只需要两个模块:bluecove与bluecove-android2
我们发现源码比jar包多了许多模块,尤其是bluecove-android2,说明增加了对Android的支持,而jar包中实则是不支持Android系统的,在后来的运行中也印证了这一点,使用jar包运行时,基本上通不过,会报各种错误,列如缺少.so等,所以最终是采用直接拷贝bluecove源码到工程中来实现的。
将https://github.com/fallowu/bluecove/tree/master/bluecove/src/main/java
与https://github.com/fallowu/bluecove/tree/master/bluecove-android2/src/main/java
中的package与java代码拷贝到工程中即可。

如何使用Bluecove库
我也是参考了网上的资料,列如:
https://oomake.com/question/2117043
https://stackoverflow.com/questions/8063178/bluetooth-obex-ftp-server-on-android-2-x
等等,众多的解决方案都提到了OBEXServer 这个类,然后我到bluecove源码中找了一下,发现实则OBEXServer是bluecove源码中写的一个使用示例,见:
https://github.com/fallowu/bluecove/blob/master/bluecove-examples/obex-server/src/main/java/net/sf/bluecove/obex/server/OBEXServer.java
如果在Android中直接使用OBEXServer.java会出现许多错误, 所以还需要改造OBEXServer才能实现FTP服务。
另外还可以参考bluecove源码中对android支持的说明,见bluecove/bluecove-android2/src/site/apt/index.apt
截取几段说明:
......
BlueCove-Android2 is additional module for BlueCove to partially support JSR-82 on Android using Android 2.x bluetooth APIs.
This module doesn t need any use of Android NDK or any native libraries. Just include its jar in classpath and it should work.
......
Before calling any JSR-82 API, be sure that you called this passing a context object (typically, the activity from which you are using BlueCove).
---
BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT, context);
---
......
OBEXServer.java改造
除了在项目中引入 Bluecove库 ,我们还需要将 OBEXServer.java 引入,但是直接引入使用时有问题,还需要进行小小的改造:
-
public final UUID OBEX_OBJECT_PUSH = new UUID(0x1105);改为
public final UUID OBEX_OBJECT_PUSH = new UUID(0x1106);为什么这么改,在bluecove源码中 BluetoothStackAndroid.java 已经给了说明:
...... private static final UUID UUID_OBEX = new UUID(0x0008); private static final UUID UUID_OBEX_OBJECT_PUSH = new UUID(0x1105); private static final UUID UUID_OBEX_FILE_TRANSFER = new UUID(0x1106); ......UUID(0x1106)才是专门传输文件的
-
run()函数中设置context object
public void run() { //add start BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context); //add end isStoped = false; LocalDevice localDevice; try { localDevice = LocalDevice.getLocalDevice(); if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) { Logger.error("Fail to set LocalDevice Discoverable"); } serverConnection = (SessionNotifier) Connector.open("btgoep://localhost:" + OBEX_OBJECT_PUSH + ";name=" + SERVER_NAME); } catch (Throwable e) { Logger.error("OBEX Server start error", e); isStoped = true; return; } ...... }我们加了一行
BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context )其中变量context为Context类型,需要启动服务时将Activity作为参数传进来。
-
去掉不需要的代码
还是在run()函数里
public void run() { //add start BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context); //add end isStoped = false; //LocalDevice localDevice; --del try { /* localDevice = LocalDevice.getLocalDevice(); if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) { Logger.error("Fail to set LocalDevice Discoverable"); } */ --del serverConnection = (SessionNotifier) Connector.open("btgoep://localhost:" + OBEX_OBJECT_PUSH + ";name=" + SERVER_NAME); } catch (Throwable e) { Logger.error("OBEX Server start error", e); isStoped = true; return; } //下面的try catch 全部去掉 /* try { ServiceRecord record = localDevice.getRecord(serverConnection); String url = record.getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false); Logger.debug("BT server url: " + url); final int OBJECT_TRANSFER_SERVICE = 0x100000; try { record.setDeviceServiceClasses(OBJECT_TRANSFER_SERVICE); } catch (Throwable e) { Logger.debug("setDeviceServiceClasses", e); } DataElement bluetoothProfileDescriptorList = new DataElement(DataElement.DATSEQ); DataElement obbexPushProfileDescriptor = new DataElement(DataElement.DATSEQ); obbexPushProfileDescriptor.addElement(new DataElement(DataElement.UUID, OBEX_OBJECT_PUSH)); obbexPushProfileDescriptor.addElement(new DataElement(DataElement.U_INT_2, 0x100)); bluetoothProfileDescriptorList.addElement(obbexPushProfileDescriptor); record.setAttributeValue(0x0009, bluetoothProfileDescriptorList); final short ATTR_SUPPORTED_FORMAT_LIST_LIST = 0x0303; DataElement supportedFormatList = new DataElement(DataElement.DATSEQ); // any type of object. supportedFormatList.addElement(new DataElement(DataElement.U_INT_1, 0xFF)); record.setAttributeValue(ATTR_SUPPORTED_FORMAT_LIST_LIST, supportedFormatList); final short UUID_PUBLICBROWSE_GROUP = 0x1002; final short ATTR_BROWSE_GRP_LIST = 0x0005; DataElement browseClassIDList = new DataElement(DataElement.DATSEQ); UUID browseClassUUID = new UUID(UUID_PUBLICBROWSE_GROUP); browseClassIDList.addElement(new DataElement(DataElement.UUID, browseClassUUID)); record.setAttributeValue(ATTR_BROWSE_GRP_LIST, browseClassIDList); localDevice.updateRecord(record); } catch (Throwable e) { Logger.error("Updating SDP", e); } */ ...... }
主要去掉了两块:localDevice.setDiscoverable 与 localDevice.updateRecord 这两个函数的调用
去掉:localDevice.setDiscoverable(DiscoveryAgent.GIAC),可防止开启服务时,手机弹出对话框提示
去掉:localDevice.updateRecord(record); 这段代码的作用,缘由可以查看 BluetoothStackAndroid.java 源码
public void rfServerUpdateServiceRecord(long handle, ServiceRecordImpl serviceRecord, boolean acceptAndOpen) throws ServiceRegistrationException {
throw new UnsupportedOperationException("Not supported yet.");
}
由于 localDevice.updateRecord(record) 最终会调用 BluetoothStackAndroid 类的 rfServerUpdateServiceRecord 函数,此函数会抛出异常,告知不支持该操作。
4.将每个Client连接变量放入集合里,方便退出时关闭
目的是在APP退出时,调用close函数,能关掉socket连接
public class OBEXServer implements Runnable {
private SessionNotifier serverConnection;
private boolean isStoped = false;
private boolean isRunning = false;
public final UUID OBEX_OBJECT_PUSH = new UUID(0x1106);
public static final String SERVER_NAME = "OBEX Object Push";
private UserInteraction interaction;
//add
private HashSet<RequestHandler> requestHandlerSet = new HashSet<RequestHandler>();
......
}
RequestHandler的connectionAccepted函数中增加一条语句:
void connectionAccepted(Connection cconn) {
Logger.debug("Received OBEX connection");
showStatus("Client connected");
this.cconn = cconn;
//add
requestHandlerSet.add(this);
if (!isConnected) {
notConnectedTimer.schedule(new TimerTask() {
public void run() {
notConnectedClose();
}
}, 1000 * 30);
}
}
RequestHandler中增加一个函数close:
void close() {
try {
if (cconn != null) {
cconn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
OBEXServer的close函数增加:
public void close() {
isStoped = true;
//add
for (RequestHandler handler : requestHandlerSet) {
handler.close();
}
requestHandlerSet.clear();
//add end
try {
if (serverConnection != null) {
serverConnection.close();
}
Logger.debug("OBEX ServerConnection closed");
} catch (Throwable e) {
Logger.error("OBEX Server stop error", e);
}
}
总结:基本上改了这四处后,OBEXServer已经支持Android作为蓝牙FTP服务端开启正常运行,其它一些细节,根据需要进行更改与优化即可,列如文件存储的目录,需要根据Android系统进行更改,要更改homePath函数。
bluecove库源码改造
如果不改造bluecove库,APP可以收到Client端的连接请求,并回复连接成功消息,但是就没有后续了,Socket会被Client端断开,所以就需要进行一些改动。
-
OBEXSessionBase.java
...... protected static int id = 1; //add public OBEXSessionBase(StreamConnection conn, OBEXConnectionParams obexConnectionParams) throws IOException { if (obexConnectionParams == null) { throw new NullPointerException("obexConnectionParams is null"); } this.isConnected = false; this.conn = conn; this.obexConnectionParams = obexConnectionParams; this.mtu = obexConnectionParams.mtu; this.connectionID = id++; //modify this.packetsCountWrite = 0; this.packetsCountRead = 0; boolean initOK = false; try { this.os = conn.openOutputStream(); this.is = conn.openInputStream(); initOK = true; } finally { if (!initOK) { try { this.close(); } catch (IOException e) { DebugLog.error("close error", e); } } } } ......增加了一个变量
protected static int id = 1;同时将
this.connectionID = 0;改为
this.connectionID = id++;源码里connectionID是一直为0,明显不正常,所以每次新建对象时connectionID自增1
另外有一处bug需要更改:
函数handleAuthenticationResponse中
if ((authChallengesSent == null) && (authChallengesSent.size() == 0)) {
throw new IOException("Authentication challenges had not been sent");
}
改为
if ((authChallengesSent == null) || (authChallengesSent.size() == 0)) {
throw new IOException("Authentication challenges had not been sent");
}
这个bug超级明显,如果用&&符,前面为null,还要去执行size,会引起空指针异常。
-
OBEXServerSessionImpl.java
增加一个函数:
private void connectHeaderTargetCopy(OBEXHeaderSetImpl paramOBEXHeaderSetImpl1, OBEXHeaderSetImpl paramOBEXHeaderSetImpl2) { if (paramOBEXHeaderSetImpl1 != null && paramOBEXHeaderSetImpl2 != null && paramOBEXHeaderSetImpl1.headerValues != null && paramOBEXHeaderSetImpl2.headerValues != null) for (Object entry : paramOBEXHeaderSetImpl1.headerValues.entrySet()) { if (((Map.Entry)entry).getKey() instanceof Integer && ((Map.Entry)entry).getValue() instanceof byte[] && ((Integer)((Map.Entry)entry).getKey()).intValue() == 70 && !paramOBEXHeaderSetImpl2.headerValues.containsKey(Integer.valueOf(74))) { paramOBEXHeaderSetImpl2.headerValues.put(Integer.valueOf(74), ((Map.Entry)entry).getValue()); break; } } }然后在函数processConnect 中调用
private void processConnect(byte[] b) throws IOException { ...... byte[] connectResponse = new byte[4]; connectResponse[0] = OBEXOperationCodes.OBEX_VERSION; connectResponse[1] = 0; /* Flags */ connectResponse[2] = OBEXUtils.hiByte(obexConnectionParams.mtu); connectResponse[3] = OBEXUtils.loByte(obexConnectionParams.mtu); connectHeaderTargetCopy(requestHeaders,replyHeaders); //add writePacketWithFlags(rc, connectResponse, replyHeaders); if (rc == ResponseCodes.OBEX_HTTP_OK) { this.isConnected = true; } }目的就是在回复Client端的连接请求时,将Client端连接请求Headers信息中的Target数据拷贝到Server端Response消息中,如果不拷贝,Client会将Socket连接断开。
注:
70:0x46 Target,操作的目的服务名
74:0x4A Who,OBEX Application标识,用于表明是否是同一个应用
Headers涉及到了OBEX协议,具体可以参考https://blog.csdn.net/feelinghappy/article/details/107967796

-
OBEXHeaderSetImpl.java
修改hasIncommingData函数
将
boolean hasIncommingData() { return headerValues.contains(new Integer(OBEX_HDR_BODY)) || headerValues.contains(new Integer(OBEX_HDR_BODY_END)); }改为
boolean hasIncommingData() { return headerValues.containsKey(new Integer(OBEX_HDR_BODY)) || headerValues.containsKey(new Integer(OBEX_HDR_BODY_END)); }此处估计是一个bug,应该判断的是headerValues的key是否包含那两个值
-
BluetoothStackAndroid.java
将函数rfServerAcceptAndOpenRfServerConnection 中的一行serverSocket.close();去掉
修改后的函数如下:
public long rfServerAcceptAndOpenRfServerConnection(long handle) throws IOException { AndroidBluetoothConnection bluetoothConnection = AndroidBluetoothConnection.getBluetoothConnection(handle); BluetoothServerSocket serverSocket = bluetoothConnection.getServerSocket(); BluetoothSocket socket = serverSocket.accept(); // serverSocket.close(); --del AndroidBluetoothConnection connection = AndroidBluetoothConnection.createConnection(socket, true); return connection.getHandle(); }rfServerAcceptAndOpenRfServerConnection函数最开始是由OBEXServer的循环语句内
handler.connectionAccepted(serverConnection.acceptAndOpen(handler));调用到的,如果在rfServerAcceptAndOpenRfServerConnection函数中关闭了serverSocket,将导致第一次成功acceptAndOpen后的后续acceptAndOpen调用全部产生异常
java.io.IOException: bt socket is not in listen state at android.bluetooth.BluetoothSocket.accept(BluetoothSocket.java:493) at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:171) at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:157) at com.intel.bluetooth.BluetoothStackAndroid.rfServerAcceptAndOpenRfServerConnection(BluetoothStackAndroid.java:461) at com.intel.bluetooth.BluetoothRFCommConnectionNotifier.acceptAndOpen(BluetoothRFCommConnectionNotifier.java:74) at com.intel.bluetooth.obex.OBEXSessionNotifierImpl.acceptAndOpen(OBEXSessionNotifierImpl.java:89) at com.intel.bluetooth.obex.OBEXSessionNotifierImpl.acceptAndOpen(OBEXSessionNotifierImpl.java:79) .......
5.OBEXServerOperationPut.java
构造函数OBEXServerOperationPut,最后增加一句:
protected OBEXServerOperationPut(OBEXServerSessionImpl session, OBEXHeaderSetImpl receivedHeaders,
boolean finalPacket) throws IOException {
super(session, receivedHeaders);
this.inputStream = new OBEXOperationInputStream(this);
processIncommingData(receivedHeaders, finalPacket);
//下面是增加的代码,主要是解决put操作时,如果接收到最后一条数据
//程序没有及时设置成最后一条,导致依旧在put操作中,没有退出,
//后续上传新的文件时,会当成上一个文件的后续,上传会失败
finalPacketReceived = finalPacket;
}
总结
应用内还需要增加权限的支持、蓝牙配对等功能,OBEXServer也可以优化,但是经过上述修改后,APP已经具备通过蓝牙FTP接收文件并保存到手机的功能。



OBEX Server start error Not supported yet 后面其他报错提示这个
我用得挺好的,你查查权限的问题
BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context);在这里 一直报错 你是否遇到了 此问题
能运行,context指向的是Activity对象
非常好的文章,为啥没人点赞呢,解决了android蓝牙的obex-ftp功能