Android 蓝牙开发OPP传输文件

君不见长松卧壑困风霜,时来屹立扶明堂。这篇文章主要讲述Android 蓝牙开发OPP传输文件相关的知识,希望能为你提供帮助。
转载请注明出处: http://blog.csdn.net/vnanyesheshou/article/details/70256004

本文已授权微信公众号 fanfan程序媛 独家发布 扫一扫文章底部的二维码或在微信搜索 fanfan程序媛 即可关注
android蓝牙功能( 传统蓝牙、ble、hid) 这三方面功能之前的博客都已经写了。现在接着了解蓝牙OPP传输文件相关功能。Android手机使用中, 经常会用到通过蓝牙分享文件给附近的朋友。那么具体是如何实现的, 大部分朋友都不是很清楚。看一下源码是如何实现该功能的。
1 BluetoothOppLauncherActivity
Android手机点击某文件进行蓝牙分享的时候, 会跳转到系统自带应用Bluetooth中。
具体文件:
packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
看一下BluetoothOppLauncherActivity是如何处理分享文件请求的。
if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) { //Check if Bluetooth is available in the beginning instead of at the end if (!isBluetoothAllowed()) { Intent in = new Intent(this, BluetoothOppBtErrorActivity.class); in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); in.putExtra(" title" , this.getString(R.string.airplane_error_title)); in.putExtra(" content" , this.getString(R.string.airplane_error_msg)); startActivity(in); finish(); return; } //..........下面接着说。 }

BluetoothOppLauncherActivity并没有界面(没有setContentView), 只是一个中转站,它根据当前蓝牙等相关状态进行跳转。Intent.ACTION_SEND和Intent.ACTION_SEND_MULTIPLE的区别是前者表示单个文件, 后者表示多个文件。这里只研究下分享单个文件, 分享单个文件懂了, 多个文件道理类似。
其中isBluetoothAllowed()函数会先判断飞行模式是否开启, 如果没有开启则返回true。如果开启, 则进行下一步判断飞行模式是否重要, 如果不重要则返回true( 说明蓝牙可以使用) 。如果重要则继续分析飞行模式下是否可以打开蓝牙, 可以打开蓝牙则返回true, 否则返回false。总的来说该函数就是判断当前蓝牙是否允许使用。不允许使用蓝牙则跳转到BluetoothOppBtErrorActivity。
接着向下:

if (action.equals(Intent.ACTION_SEND)) { //单个文件 final String type = intent.getType(); final Uri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM); CharSequence extra_text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); if (stream != null & & type != null) { //分享文件 Thread t = new Thread(new Runnable() { public void run() { BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this) .saveSendingFileInfo(type,stream.toString(), false); launchDevicePicker(); finish(); } }); t.start(); return; } else if (extra_text != null & & type != null) { //分享text字符串, 没有文件 final Uri fileUri = creatFileForSharedContent(this, extra_text); //创建文件,将内容写入文件 if (fileUri != null) { Thread t = new Thread(new Runnable() { public void run() { BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this) .saveSendingFileInfo(type,fileUri.toString(), false); launchDevicePicker(); finish(); } }); t.start(); return; } //......... }

使用过Android系统分享的应该知道, 其支持文件(图片、视频等)、字符串。而这里会对文件、字符串进行区分处理, 字符串则先创建文件然后在进行分享。
launchDevicePicker()函数中先判断蓝牙是否开启。
如果蓝牙没有开启则跳转到BluetoothOppBtEnableActivity显示dialog( 询问是否开启蓝牙) , 点击取消则则退出, 点击打开则打开蓝牙并跳到BluetoothOppBtEnablingActivity( 该activity主要显示一个progress dialog) 。当蓝牙打开, 则BluetoothOppBtEnablingActivity 界面finish。BluetoothOppReceiver广播接收者接收到蓝牙开启, 跳转到DevicePickerActivity界面( 系统Settings应用) 。
如果蓝牙已开启, 则直接跳转到跳转到DevicePickerActivity界面( 系统Settings应用) 。
launchDevicePicker()下的跳转代码:

//ACTION_LAUNCH= " android.bluetooth.devicepicker.action.LAUNCH" Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); in1.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false); in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, BluetoothDevicePicker.FILTER_TYPE_TRANSFER); in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, Constants.THIS_PACKAGE_NAME); in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, BluetoothOppReceiver.class.getName()); startActivity(in1);

系统Settings应用中AndroidManifest.xml中发现对应action的DevicePickerActivity, 所以该跳转会跳转到系统Settings应用中的DevicePickerActivity中。
< activity android:name= " .bluetooth.DevicePickerActivity" android:uiOptions= " splitActionBarWhenNarrow" android:theme= " @ android:style/Theme.Holo.DialogWhenLarge" android:label= " @ string/device_picker" android:clearTaskOnLaunch= " true" > < intent-filter> < action android:name= " android.bluetooth.devicepicker.action.LAUNCH" /> < category android:name= " android.intent.category.DEFAULT" /> < /intent-filter> < /activity>

2 DevicePicker
DevicePickerActivity中代码很简单, 只是设置了布局。
setContentView(R.layout.bluetooth_device_picker);
bluetooth_device_picker.xml中有一个fragment指向DevicePickerFragment, 也就是主要的处理在DevicePickerFragment中。
DevicePickerFragment界面会显示出配对、扫描到的蓝牙列表。可以点击一个设备进行分享文件。

void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { mLocalAdapter.stopScanning(); //停止扫描 LocalBluetoothPreferences.persistSelectedDeviceInPicker( getActivity(), mSelectedDevice.getAddress()); if ((btPreference.getCachedDevice().getBondState() = = BluetoothDevice.BOND_BONDED) || !mNeedAuth) { sendDevicePickedIntent(mSelectedDevice); finish(); } else { super.onDevicePreferenceClick(btPreference); } }

点击设备, 会判断是否是绑定状态, 或者mNeedAuth为false。mNeedAuth是通过intent传过来的值为false。所以满足条件。
接着看sendDevicePickedIntent()。该函数就是发了一个广播。

private void sendDevicePickedIntent(BluetoothDevice device) { //" android.bluetooth.devicepicker.action.DEVICE_SELECTED" Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); if (mLaunchPackage != null & & mLaunchClass != null) { intent.setClassName(mLaunchPackage, mLaunchClass); } getActivity().sendBroadcast(intent); }

3 BluetoothOppReceiver
查看系统应用Bluetooth中的BluetoothOppReceiver类中对此广播进行了处理。但是Bluetooth中的AndroidManifest.xml中该广播接收者的注册并没有添加此action。但是却可以接收此广播。原因应该是该广播发送时携带了包名、类名。
< receiver android:process= " @ string/process" android:exported= " true" android:name= " .opp.BluetoothOppReceiver" android:enabled= " @ bool/profile_supported_opp" > < intent-filter> < action android:name= " android.bluetooth.adapter.action.STATE_CHANGED" /> < !--action android:name= " android.intent.action.BOOT_COMPLETED" /--> < action android:name= " android.btopp.intent.action.OPEN_RECEIVED_FILES" /> < /intent-filter> < /receiver>

【Android 蓝牙开发OPP传输文件】BluetoothOppReceiver收到此广播后的主要处理代码如下, 将此条记录添加到数据库。
// Insert transfer session record to database mOppManager.startTransfer(remoteDevice);

BluetoothOppManager对象调用startTransfer方法。在startTransfer方法中创建一个InsertShareInfoThread线程并开始运行。
InsertShareInfoThread线程中区分分享的是一个文件还是多个文件。我们这里只看下处理单个文件insertSingleShare()函数。

if (mIsMultiple) {//多个文件 insertMultipleShare(); } else { //单个文件 insertSingleShare(); }private void insertSingleShare() { ContentValues values = new ContentValues(); values.put(BluetoothShare.URI, mUri); values.put(BluetoothShare.MIMETYPE, mTypeOfSingleFile); values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress()); if (mIsHandoverInitiated) { values.put(BluetoothShare.USER_CONFIRMATION, BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED); } final Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values); }

由mContext.getContentResolver().insert()可知其有对应的provider。BluetoothOppProvider继承了ContextProvider。查看BluetoothOppProvider中的insert方法。
public Uri insert(Uri uri, ContentValues values) { ..... if (rowID != -1) { context.startService(new Intent(context, BluetoothOppService.class)); ret = Uri.parse(BluetoothShare.CONTENT_URI + " /" + rowID); context.getContentResolver().notifyChange(uri, null); }

由上可知通过蓝牙分享的时候会start BluetoothOppService。
4 BluetoothOppService
在BluetoothOppService中会监听数据库字段( BluetoothShare.CONTENT_URI) 的变化, 调用updateFromProvider()函数进行处理。onCreate()和onStartCommand()函数都会调用updateFromProvider()。
updateFromProvider() -> 创建线程UpdateThread -> insertShare()。

private void insertShare(Cursor cursor, int arrayPos) { if (info.isReadyToStart()) { if (info.mDirection = = BluetoothShare.DIRECTION_OUTBOUND) { //向外分享、发送 /* 检查文件是否存在 */ } } if (mBatchs.size() = = 0) { if (info.mDirection = = BluetoothShare.DIRECTION_OUTBOUND){//向外分享、发送 mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch); } else if (info.mDirection = = BluetoothShare.DIRECTION_INBOUND) { //接收 mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch, mServerSession); }if (info.mDirection = = BluetoothShare.DIRECTION_OUTBOUND & & mTransfer != null) { mTransfer.start(); } else if (info.mDirection = = BluetoothShare.DIRECTION_INBOUND & & mServerTransfer != null) { mServerTransfer.start(); } } //........ }

5 BluetoothOppTransfer
这里只说向外发送、分享。接着看BluetoothOppTransfer。
public void start() { //检查蓝牙是否打开, 保证安全 if (!mAdapter.isEnabled()) { return; } if (mHandlerThread = = null) { //...... if (mBatch.mDirection = = BluetoothShare.DIRECTION_OUTBOUND) { /* for outbound transfer, we do connect first */ startConnectSession(); } //.... } }

startConnectSession()函数中开始向远端设备进行连接, 该函数中主要就是创建SocketConnectThread线程, 用来连接其他设备。
SocketConnectThread线程主要代码:

try { //创建BluetoothSocket btSocket = device.createInsecureRfcommSocketToServiceRecord(BluetoothUuid.ObexObjectPush.getUuid()); } catch (IOException e1) {//.... } try { btSocket.connect(); //; 连接设备 BluetoothOppRfcommTransport transport; transport = new BluetoothOppRfcommTransport(btSocket); BluetoothOppPreference.getInstance(mContext).setName(device, device.getName()); mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget(); } catch (IOException e) {//.... }

这里先创建BluetoothSocket, 然后通过BluetoothSocket进行连接。
连接成功后, startObexSession()-> new BluetoothOppObexClientSession -> BluetoothOppObexClientSession .start()

6 BluetoothOppObexClientSession
BluetoothOppObexClientSession类说明该设备作为obex client, 向server发送文件。该类中主要功能: obex连接、发送分享文件的信息, 发送数据等。
start() -> 创建ClientThread线程并运行 -> connect()。
connect()函数中, 通过mTransport1( BluetoothOppRfcommTransport类型, 该类型中主要包含之前创建的BluetoothSocket) 对象, 创建client session, 连接远端设备。

private void connect(int numShares) { try {//创建obex client mCs = new ClientSession(mTransport1); mConnected = true; } catch (IOException e1) { } if (mConnected) { mConnected = false; HeaderSet hs = new HeaderSet(); //obex 连接携带信息 hs.setHeader(HeaderSet.COUNT, (long) numShares); //文件数量 synchronized (this) { mWaitingForRemote = true; } try { //obex连接 mCs.connect(hs); mConnected = true; } catch (IOException e) { } } //..... }

obex连接成功后, 调用doSend(),该函数中先检查下文件是否存在, 然后查看连接状态, 连接状态下并且存在文件则sendFile才真正的开始发送文件。之会将相应的状态发送到BluetoothOppTransfer中。
private void doSend() { int status = BluetoothShare.STATUS_SUCCESS; while (mFileInfo = = null) { //检查文件是否存在 try { Thread.sleep(50); } catch (InterruptedException e) { status = BluetoothShare.STATUS_CANCELED; } } //检查连接状态 if (!mConnected) { status = BluetoothShare.STATUS_CONNECTION_ERROR; } if (status = = BluetoothShare.STATUS_SUCCESS) { /* 发送文件*/ if (mFileInfo.mFileName != null) { status = sendFile(mFileInfo); } else { status = mFileInfo.mStatus; } waitingForShare = true; } else { Constants.updateShareStatus(mContext1, mInfo.mId, status); } //发送此次操作是否成功等信息。 }

真正的发送文件是在sendFile()函数中。不过该函数太长就不全贴出来了, 只说一下重要的地方。
1 发送文件信息
HeaderSet request = new HeaderSet(); request.setHeader(HeaderSet.NAME, fileInfo.mFileName); //文件名 request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype); //文件类型 request.setHeader(HeaderSet.LENGTH, fileInfo.mLength); //文件大小 //通过obex发送传递文件请求 putOperation = (ClientOperation)mCs.put(request); //putOperation类型为ClientOperation, 具体java.obex包下的类没有向外透漏, 不太清楚是具体怎么回事。

2 获取obex层输入输出流
//获取输入输出流。 outputStream = putOperation.openOutputStream(); inputStream = putOperation.openInputStream();

3 发送第一个包
//从文件中读取内容 BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000); readLength = readFully(a, buffer, outputBufferSize); //先向远程设备发送第一个包 该操作会阻塞等待远端设备的接收读取。 outputStream.write(buffer, 0, readLength); position + = readLength; 如果文件太小, 一个包就已经发送完, 则将输出流关闭。outputStream.close();

4 查看回应
接着查看远端设备的回应, 是否接受。

/* check remote accept or reject */ responseCode = putOperation.getResponseCode(); if (responseCode = = ResponseCodes.OBEX_HTTP_CONTINUE || responseCode = = ResponseCodes.OBEX_HTTP_OK) { //接收 okToProceed = true; updateValues = new ContentValues(); updateValues.put(BluetoothShare.CURRENT_BYTES, position); mContext1.getContentResolver().update(contentUri, updateValues, null, null); } else {//拒绝接收 Log.i(TAG, " Remote reject, Response code is " + responseCode); }

5 判断发送数据
接着循环判断、从文件读取数据、发送数据。

while (!mInterrupted & & okToProceed & & (position != fileInfo.mLength)) { readLength = a.read(buffer, 0, outputBufferSize); outputStream.write(buffer, 0, readLength); /* check remote abort */ responseCode = putOperation.getResponseCode(); if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE & & responseCode != ResponseCodes.OBEX_HTTP_OK) { okToProceed = false; } else { position + = readLength; //更行进度 updateValues = new ContentValues(); updateValues.put(BluetoothShare.CURRENT_BYTES, position); mContext1.getContentResolver().update(contentUri, updateValues, null, null); } }

在之后就是一些状态的处理了。到此通过蓝牙分享文件到流程基本上过了一遍, 其中还有许多状态、进度等相关功还没能研究透彻, 之后再继续研究。
欢迎扫一扫关注我的微信公众号, 定期推送优质技术文章:
Android 蓝牙开发OPP传输文件

文章图片


    推荐阅读