最近有一個(gè)需求呕臂,要求在Android APP中朗儒,通過藍(lán)牙FTP協(xié)議實(shí)現(xiàn)文件接收功能。為此吮炕,找了很多資料,發(fā)現(xiàn)比較簡(jiǎn)單的實(shí)現(xiàn)方案是采用Bluecove庫通過OBEX協(xié)議來實(shí)現(xiàn)访得。
Bluecove庫中已經(jīng)實(shí)現(xiàn)了OBEX協(xié)議的解析龙亲,同時(shí)會(huì)調(diào)用Android系統(tǒng)的 BluetoothServerSocket與 BluetoothSocket 進(jìn)行藍(lán)牙通信監(jiān)聽與數(shù)據(jù)通信。所以利用該庫悍抑,直接調(diào)用相關(guān)接口即可鳄炉,使用比較簡(jiǎn)單。
Bluecove庫下載
-
使用implementation
在Android Studio中可使用 implementation 的方式自動(dòng)下載bluecove.jar
在build.gradle中加入(當(dāng)前最新版本是2.1.0)
implementation 'net.sf.bluecove:bluecove:2.1.0'
-
網(wǎng)上下載jar包
從https://mvnrepository.com/artifact/net.sf.bluecove/bluecove/2.1.0可下載最新的jar包搜骡,點(diǎn)擊下圖紅圈標(biāo)記處直接下載拂盯,這一方式其實(shí)與方式1是同一源
-
直接到github上下載源碼
只需要兩個(gè)模塊:bluecove與bluecove-android2
我們發(fā)現(xiàn)源碼比jar包多了很多模塊,尤其是bluecove-android2记靡,說明增加了對(duì)Android的支持谈竿,而jar包中其實(shí)是不支持Android系統(tǒng)的,在后來的運(yùn)行中也印證了這一點(diǎn)摸吠,使用jar包運(yùn)行時(shí)空凸,基本上通不過,會(huì)報(bào)各種錯(cuò)誤寸痢,比如缺少.so等呀洲,所以最終是采用直接拷貝bluecove源碼到工程中來實(shí)現(xiàn)的。
將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庫
我也是參考了網(wǎng)上的資料道逗,比如:
https://oomake.com/question/2117043
https://stackoverflow.com/questions/8063178/bluetooth-obex-ftp-server-on-android-2-x
等等,眾多的解決方案都提到了OBEXServer 這個(gè)類献烦,然后我到bluecove源碼中找了一下滓窍,發(fā)現(xiàn)其實(shí)OBEXServer是bluecove源碼中寫的一個(gè)使用示例,見:
如果在Android中直接使用OBEXServer.java會(huì)出現(xiàn)很多錯(cuò)誤仿荆, 所以還需要改造OBEXServer才能實(shí)現(xiàn)FTP服務(wù)贰您。
另外還可以參考bluecove源碼中對(duì)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改造
除了在項(xiàng)目中引入 Bluecove庫 ,我們還需要將 OBEXServer.java 引入锦亦,但是直接引入使用時(shí)有問題舶替,還需要進(jìn)行小小的改造:
-
public final UUID OBEX_OBJECT_PUSH = new UUID(0x1105);
改為
public final UUID OBEX_OBJECT_PUSH = new UUID(0x1106);
為什么這么改,在bluecove源碼中 BluetoothStackAndroid.java 已經(jīng)給了說明:
...... 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()函數(shù)中設(shè)置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類型杠园,需要啟動(dòng)服務(wù)時(shí)將Activity作為參數(shù)傳進(jìn)來顾瞪。
-
去掉不需要的代碼
還是在run()函數(shù)里
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 這兩個(gè)函數(shù)的調(diào)用
去掉:localDevice.setDiscoverable(DiscoveryAgent.GIAC),可防止開啟服務(wù)時(shí)抛蚁,手機(jī)彈出對(duì)話框提示
去掉:localDevice.updateRecord(record); 這段代碼的作用陈醒,原因可以查看 BluetoothStackAndroid.java 源碼
public void rfServerUpdateServiceRecord(long handle, ServiceRecordImpl serviceRecord, boolean acceptAndOpen) throws ServiceRegistrationException {
throw new UnsupportedOperationException("Not supported yet.");
}
因?yàn)?localDevice.updateRecord(record) 最終會(huì)調(diào)用 BluetoothStackAndroid 類的 rfServerUpdateServiceRecord 函數(shù),此函數(shù)會(huì)拋出異常瞧甩,告知不支持該操作钉跷。
4.將每個(gè)Client連接變量放入集合里,方便退出時(shí)關(guān)閉
目的是在APP退出時(shí)肚逸,調(diào)用close函數(shù)爷辙,能關(guān)掉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函數(shù)中增加一條語句:
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中增加一個(gè)函數(shù)close:
void close() {
try {
if (cconn != null) {
cconn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
OBEXServer的close函數(shù)增加:
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);
}
}
總結(jié):基本上改了這四處后,OBEXServer已經(jīng)支持Android作為藍(lán)牙FTP服務(wù)端開啟正常運(yùn)行朦促,其它一些細(xì)節(jié)膝晾,根據(jù)需要進(jìn)行更改與優(yōu)化即可,比如文件存儲(chǔ)的目錄务冕,需要根據(jù)Android系統(tǒng)進(jìn)行更改血当,要更改homePath函數(shù)。
bluecove庫源碼改造
如果不改造bluecove庫禀忆,APP可以收到Client端的連接請(qǐng)求臊旭,并回復(fù)連接成功消息,但是就沒有后續(xù)了油湖,Socket會(huì)被Client端斷開巍扛,所以就需要進(jìn)行一些改動(dòng)。
-
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); } } } } ......
增加了一個(gè)變量
protected static int id = 1;
同時(shí)將
this.connectionID = 0;
改為
this.connectionID = id++;
源碼里connectionID是一直為0乏德,明顯不正常撤奸,所以每次新建對(duì)象時(shí)connectionID自增1
另外有一處bug需要更改:
函數(shù)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");
}
這個(gè)bug非常明顯,如果用&&符喊括,前面為null胧瓜,還要去執(zhí)行size,會(huì)引起空指針異常郑什。
-
OBEXServerSessionImpl.java
增加一個(gè)函數(shù):
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; } } }
然后在函數(shù)processConnect 中調(diào)用
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; } }
目的就是在回復(fù)Client端的連接請(qǐng)求時(shí)府喳,將Client端連接請(qǐng)求Headers信息中的Target數(shù)據(jù)拷貝到Server端Response消息中,如果不拷貝蘑拯,Client會(huì)將Socket連接斷開钝满。
注:
70:0x46 Target兜粘,操作的目的服務(wù)名
74:0x4A Who,OBEX Application標(biāo)識(shí)弯蚜,用于表明是否是同一個(gè)應(yīng)用
Headers涉及到了OBEX協(xié)議孔轴,具體可以參考https://blog.csdn.net/feelinghappy/article/details/107967796
-
OBEXHeaderSetImpl.java
修改hasIncommingData函數(shù)
將
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)); }
此處估計(jì)是一個(gè)bug,應(yīng)該判斷的是headerValues的key是否包含那兩個(gè)值
-
BluetoothStackAndroid.java
將函數(shù)rfServerAcceptAndOpenRfServerConnection 中的一行serverSocket.close();去掉
修改后的函數(shù)如下:
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函數(shù)最開始是由OBEXServer的循環(huán)語句內(nèi)
handler.connectionAccepted(serverConnection.acceptAndOpen(handler));
調(diào)用到的碎捺,如果在rfServerAcceptAndOpenRfServerConnection函數(shù)中關(guān)閉了serverSocket路鹰,將導(dǎo)致第一次成功acceptAndOpen后的后續(xù)acceptAndOpen調(diào)用全部產(chǎn)生異常
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
構(gòu)造函數(shù)OBEXServerOperationPut,最后增加一句:
protected OBEXServerOperationPut(OBEXServerSessionImpl session, OBEXHeaderSetImpl receivedHeaders,
boolean finalPacket) throws IOException {
super(session, receivedHeaders);
this.inputStream = new OBEXOperationInputStream(this);
processIncommingData(receivedHeaders, finalPacket);
//下面是增加的代碼收厨,主要是解決put操作時(shí)晋柱,如果接收到最后一條數(shù)據(jù)
//程序沒有及時(shí)設(shè)置成最后一條,導(dǎo)致仍然在put操作中诵叁,沒有退出雁竞,
//后續(xù)上傳新的文件時(shí),會(huì)當(dāng)成上一個(gè)文件的后續(xù)黎休,上傳會(huì)失敗
finalPacketReceived = finalPacket;
}
總結(jié)
應(yīng)用內(nèi)還需要增加權(quán)限的支持浓领、藍(lán)牙配對(duì)等功能,OBEXServer也可以優(yōu)化势腮,但是經(jīng)過上述修改后,APP已經(jīng)具備通過藍(lán)牙FTP接收文件并保存到手機(jī)的功能漫仆。