最近有一个需求,要求在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 requestHandlerSet = new HashSet();
......
}
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接收文件并保存到手机的功能。