Android蓝牙OBEX FTP服务端实现

最近有一个需求,要求在Android APP中,通过蓝牙FTP协议实现文件接收功能。为此,找了许多资料,发现比较简单的实现方案是采用Bluecove库通过OBEX协议来实现。

Bluecove库中已经实现了OBEX协议的解析,同时会调用Android系统的 BluetoothServerSocketBluetoothSocket 进行蓝牙通信监听与数据通信。所以利用该库,直接调用相关接口即可,使用比较简单。

Bluecove库下载

  1. 使用implementation

    在Android Studio中可使用 implementation 的方式自动下载bluecove.jar

    在build.gradle中加入(当前最新版本是2.1.0)

    implementation  net.sf.bluecove:bluecove:2.1.0 
    

  2. 网上下载jar包

    从https://mvnrepository.com/artifact/net.sf.bluecove/bluecove/2.1.0可下载最新的jar包,点击下图红圈标记处直接下载,这一方式实则与方式1是同一源

Android蓝牙OBEX FTP服务端实现

  1. 直接到github上下载源码

    https://github.com/fallowu/bluecove

只需要两个模块:bluecovebluecove-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代码拷贝到工程中即可。

Android蓝牙OBEX FTP服务端实现

如何使用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 引入,但是直接引入使用时有问题,还需要进行小小的改造:

  1. 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)才是专门传输文件的

  2. 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作为参数传进来。

  3. 去掉不需要的代码

    还是在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.setDiscoverablelocalDevice.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>();

    ......

}

RequestHandlerconnectionAccepted函数中增加一条语句:

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();
    }
}

OBEXServerclose函数增加:

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端断开,所以就需要进行一些改动。

  1. 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,会引起空指针异常。

  1. 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

Android蓝牙OBEX FTP服务端实现

  1. 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是否包含那两个值

  2. 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接收文件并保存到手机的功能。

© 版权声明

相关文章

6 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    诗和远方 读者

    OBEX Server start error Not supported yet 后面其他报错提示这个

    无记录
  • 头像
    深圳坪山农家乐 读者

    我用得挺好的,你查查权限的问题

    无记录
  • 头像
    巧克小熊加點醬 投稿者

    BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context);在这里 一直报错 你是否遇到了 此问题

    无记录
  • 头像
    未完恋暖诗篇 投稿者

    能运行,context指向的是Activity对象

    无记录
  • 头像
    小鸡说这是一个抽象又恶俗的长ID 投稿者

    非常好的文章,为啥没人点赞呢,解决了android蓝牙的obex-ftp功能

    无记录