蓝牙开发经验小结——自动文件传输(OBEX)

场景:控制端——普通手机;被控制端——XX设备(无屏幕、无法用户操作、有系统权限)
网上关于文件传输实现的文章较少,没有发现满足我需求的资料,于是我索性深入到系统源码里头,研究了系统蓝牙(com.android.bluetooth不同的平台包名可能有差异)是如何实现文件收发的,然后再设计出自己的实现方案。
对于已经实现了蓝牙socket通讯(BluetoothChatService和BluetoothUtil)的小伙伴们,很容易想到的文件传输方式是采用文件流,1.通过socket获取输入输出流来处理传输(分别使用getInputStream()和getOutputStream())。2.用read(byte[])和write(byte[])来实现读写。然而实际上并没有那么简单,如“数据包截断”问题,以及传输过程中穿插其他信令交互的问题等等。如果基于蓝牙socket,通过文件流的形式传输,要做到比较可靠,其实就相当于你要实现OBEX协议(对象交换)的具体内容,复杂度和工作量还是不小的。
请注意,普通手机端,由于可以用户操作,依据通用API直接调用com.android.bluetooth的文件传输功能即可,这里说的是XX设备端,由于无法交互,需要程序自动确认传输的实现方式。
直接上代码吧,XX设备端,发送文件的处理:

    /**
     * 通过系统的蓝牙文件传输功能,基于OBEX协议,向外发送文件
     * @param file
     * @param destination
     */
    public void sendFileByBluetooth(File file, String destination) {
                String uri = "file://" + file.getAbsolutePath();
        Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
        sharingIntent.setType("*/*");
        sharingIntent.setComponent(new ComponentName("com.android.bluetooth", "com.android.bluetooth.opp.BluetoothOppLauncherActivity"));
        sharingIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(uri));
        App.getInstance().startActivity(sharingIntent);
        //以上代码看上去和第三方应用调用蓝牙发送没什么太多区别

        //下面的代码,就是直接跳过原本需要用户手动操作的步骤了

        //稍等片刻,提高发送成功率的关键
        try {
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
            logd(e);
        }

        Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);//需要系统应用uid=1000,才可以发送
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, getBTDevice(destination));
        intent.setClassName("com.android.bluetooth", "com.android.bluetooth.opp.BluetoothOppReceiver");
        App.getInstance().sendBroadcast(intent);
//这一段发送了一个定向广播,告诉com.android.bluetooth已经选择了一个设备

//下面这一段,就是要把弹出的蓝牙选择界面自动去除(模拟按下返回键)
        new Thread(new Runnable() {//自动去掉蓝牙选择的界面
            @Override
            public void run() {
                boolean cover = false;//被遮挡
                while (true) {
                    if (!isAppOnForeground(App.getInstance(), App.getInstance().getPackageName())) {
                        try {
                            Instrumentation inst = new Instrumentation();
                            inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        cover = true;
                    }

                    if (cover && isAppOnForeground(App.getInstance(), App.getInstance().getPackageName())) {
                        logd("break");
                        break;
                    } else {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            logd(e);
                        }
                    }
                }
            }
        }).start();
    }

XX设备端,接收文件的处理:
(使用了ContentObserver ,至于为什么可查看源码,这里不多解释)

    String sendFileName = "";
    File targetFileReceive = null;
    File targetFileSend = null;
    boolean transferFlag = false;
    private ContentObserver btObserver = new ContentObserver(mHandler) {//用于自动确认并接收蓝牙文件
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            logd("btObserver onChange.");//需要申请权限android.permission.ACCESS_BLUETOOTH_SHARE,并且需要同Bluetooth的签名,即系统签名
            if(!transferFlag){
                return;
            }

            Cursor cursor = App.getInstance().getContentResolver().query(BluetoothShare.CONTENT_URI, null, null, null, BluetoothShare._ID);

            if(cursor == null){
                logd("cursor null.");
                return;
            }

            cursor.moveToLast();//直接跳至最后一条记录(最新记录)
            int id = cursor.getInt(cursor.getColumnIndex(BluetoothShare._ID));
            int direction = cursor.getInt(cursor.getColumnIndex(BluetoothShare.DIRECTION));
            int user_confirm = cursor.getInt(cursor.getColumnIndex(BluetoothShare.USER_CONFIRMATION));
            String fileName = cursor.getString(cursor.getColumnIndex(BluetoothShare.FILENAME_HINT));
            StringBuilder stringBuilder = new StringBuilder();
            if (direction == BluetoothShare.DIRECTION_INBOUND && user_confirm == BluetoothShare.USER_CONFIRMATION_PENDING) {
                stringBuilder.append("a new incoming file confirm,");
                if (!TextUtils.isEmpty(sendFileName) && sendFileName.equals(fileName)) {
                    if (fileName != null) {
                        targetFileReceive = new File(FileConfig.FILE_BLUETOOTH, fileName);
                        if(targetFileReceive.exists()){//避免有重名的文件,先删除
                            targetFileReceive.delete();
                        }
                    }
                    ContentValues mUpdateValues = new ContentValues();
                    mUpdateValues.put(BluetoothShare.USER_CONFIRMATION, BluetoothShare.USER_CONFIRMATION_CONFIRMED);
                    App.getInstance().getContentResolver().update(BluetoothShare.CONTENT_URI, mUpdateValues, null, null);
                    stringBuilder.append("confirm it.");
                    startBTTransferMonitering(id, direction);//开始监控接收过程
                } else {
                    stringBuilder.append("refuse it.");
                    ContentValues mUpdateValues = new ContentValues();
                    mUpdateValues.put(BluetoothShare.USER_CONFIRMATION, BluetoothShare.USER_CONFIRMATION_DENIED);
                    App.getInstance().getContentResolver().update(BluetoothShare.CONTENT_URI, mUpdateValues, null, null);
                }
                logd(stringBuilder.append("id = ").append(id).toString());//打印该条记录
            } else if (direction == BluetoothShare.DIRECTION_OUTBOUND && user_confirm == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED) {
                logd(stringBuilder.append("a new outcoming file request:").append("id = ").append(id));//打印该条记录
                startBTTransferMonitering(id, direction);//开始监控发送过程
            }

            for (int i = 0; i < 3 && !cursor.isBeforeFirst(); i++) {//打印最后三条记录
                logd(buildBtCursorInfo(cursor));
                cursor.moveToPrevious();
            }

            if (cursor != null) {
                cursor.close();
            }
        }
    };

    private String buildBtCursorInfo(Cursor cursor){
        if(cursor == null){
            return null;
        }
        StringBuilder stringBuilder = new StringBuilder();
        int id = cursor.getInt(cursor.getColumnIndex(BluetoothShare._ID));
        String uri = cursor.getString(cursor.getColumnIndex(BluetoothShare.URI));
        String fileName = cursor.getString(cursor.getColumnIndex(BluetoothShare.FILENAME_HINT));
        String data = cursor.getString(cursor.getColumnIndex(BluetoothShare._DATA));
        String mineType = cursor.getString(cursor.getColumnIndex(BluetoothShare.MIMETYPE));
        int direction = cursor.getInt(cursor.getColumnIndex(BluetoothShare.DIRECTION));
        String destination = cursor.getString(cursor.getColumnIndex(BluetoothShare.DESTINATION));
        int visibility = cursor.getInt(cursor.getColumnIndex(BluetoothShare.VISIBILITY));
        int user_confirm = cursor.getInt(cursor.getColumnIndex(BluetoothShare.USER_CONFIRMATION));
        int status = cursor.getInt(cursor.getColumnIndex(BluetoothShare.STATUS));
        int total_bytes = cursor.getInt(cursor.getColumnIndex(BluetoothShare.TOTAL_BYTES));
        int current_bytes = cursor.getInt(cursor.getColumnIndex(BluetoothShare.CURRENT_BYTES));
        int timeStamp = cursor.getInt(cursor.getColumnIndex(BluetoothShare.TIMESTAMP));
        int media_scanned = cursor.getInt(cursor.getColumnIndex("scanned"));
        stringBuilder.append(BluetoothShare._ID).append(" = ").append(id).append(",")
                .append(BluetoothShare.URI).append(" = ").append(uri).append(",")
                .append(BluetoothShare.FILENAME_HINT).append(" = ").append(fileName).append(",")
                .append(BluetoothShare._DATA).append(" = ").append(data).append(",")
                .append(BluetoothShare.MIMETYPE).append(" = ").append(mineType).append(",")
                .append(BluetoothShare.DIRECTION).append(" = ").append(direction).append(",")
                .append(BluetoothShare.DESTINATION).append(" = ").append(destination).append(",")
                .append(BluetoothShare.VISIBILITY).append(" = ").append(visibility).append(",")
                .append(BluetoothShare.USER_CONFIRMATION).append(" = ").append(user_confirm).append(",")
                .append(BluetoothShare.STATUS).append(" = ").append(status).append(",")
                .append(BluetoothShare.TOTAL_BYTES).append(" = ").append(total_bytes).append(",")
                .append(BluetoothShare.CURRENT_BYTES).append(" = ").append(current_bytes).append(",")
                .append(BluetoothShare.TIMESTAMP).append(" = ").append(timeStamp).append(",")
                .append("scanned").append(" = ").append(media_scanned);
        return stringBuilder.toString();
    }

    private void startBTTransferMonitering(final int id, final int direction){
        new Thread(new Runnable() {
            @Override
            public void run() {
                logt("bt Transfer Monitor Thread start.");
                Cursor cursor = null;
                long start = System.currentTimeMillis();
                boolean success = false;

                while (true){
                    cursor = App.getInstance().getContentResolver().query(BluetoothShare.CONTENT_URI, null, null, null, BluetoothShare._ID);

                    if(cursor == null){
                        logt("bt transfer monitering failed, cursor null.");
                        break;
                    } else {
                        for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()){
                            if(cursor.getInt(cursor.getColumnIndex(BluetoothShare._ID)) == id){
                                break;
                            }
                        }
                        //logt(buildBtCursorInfo(cursor));
                        int status = cursor.getInt(cursor.getColumnIndex(BluetoothShare.STATUS));
                        if (BluetoothShare.isStatusCompleted(status)) {
                            if (BluetoothShare.isStatusSuccess(status)) {
                                success = true;
                                logt("Status Success.");
                            } else if (BluetoothShare.isStatusError(status)) {
                                if (BluetoothShare.isStatusClientError(status)) {
                                    logt("Status Client Error.");
                                } else if (BluetoothShare.isStatusServerError(status)) {
                                    logt("Status Server Error.");
                                } else {
                                    logt("Status Error");
                                }
                            } else {
                                logt("Status Completed.");
                            }

                            break;
                        } else if (BluetoothShare.isStatusInformational(status)) {
                            //logt("Status Informational.");
                        } else if (BluetoothShare.isStatusSuspended(status)) {
                            logt("Status Suspended.");
                        } else {
                            logt("Status = " + status);
                        }
                    }

                    if(System.currentTimeMillis() - start >= 10*60*1000){//十分钟超时
                        break;
                    }

                    try{
                        Thread.sleep(500);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                        logt(e);
                    }
                }

                if (cursor != null) {
                    cursor.close();
                }

                if (direction == BluetoothShare.DIRECTION_OUTBOUND) {//发送文件结束
                    String res = buildBtMessage(success ? TcpConfig.BT_COPY_FILE : TcpConfig.BT_DELETE_FILE, targetFileSend.getName());
                    if (!TextUtils.isEmpty(res)) {
                        sendMessage(res);
                    }
                    if (targetFileSend != null && targetFileSend.exists()) {//删除压缩包
                        targetFileSend.delete();
                        targetFileSend = null;
                    }
                } else {//接收文件结束
                    if (targetFileReceive != null && targetFileReceive.exists()) {//把目标文件(apk升级包)剪切至Download目录
                        if (success) {
                            ShellUtil.execCommand("cp " + targetFileReceive.getAbsolutePath() + " " + FileConfig.FILE_DOWNLOAD + targetFileReceive.getName(), false);
                            //FileUtil.copyFile(targetFileReceive.getAbsolutePath(), FileConfig.FILE_DOWNLOAD + targetFileReceive.getName(), false);
                        }
                        sendMessage(buildBtMessage(TcpConfig.BT_SEND_APK, success ? "成功" : "失败"));
                        targetFileReceive.delete();
                        targetFileReceive = null;
                    }
                }

                logt("bt Transfer Monitor Thread quit. Duration time = "
                        + getSimpleFormat(System.currentTimeMillis() - start));
            }

            private void logt(Object o) {
                logd("Thread_" + Thread.currentThread().getId() + ":" + o);
            }
        }).start();
    }

    public String getSimpleFormat(long cost){
        if (cost >= 60 * 60 * 1000) {
            return String.valueOf(UniversalUtil.getDoubleFomat((float) cost / (60 * 60 * 1000), 3)) + " h";
        } else if (cost >= 60 * 1000) {
            return String.valueOf(UniversalUtil.getDoubleFomat((float) cost / (60 * 1000), 3)) + " min";
        } else if(cost>=1000){
            return String.valueOf(UniversalUtil.getDoubleFomat((float) cost / 1000, 3)) + " s";
        } else {
            return String.valueOf(cost) + "ms";
        }
    }

以上实现方案,纯属自行研究结果,如有更优方案,欢迎交流指教。
根据实测经验,蓝牙传输的速率,相比于热点连接,会慢很多。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值