场景:控制端——普通手机;被控制端——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);