本文已授权微信公众号 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 =newIntent(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)) {//单个文件
- finalString type = intent.getType();finalUri stream = (Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM);
- CharSequence extra_text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);if(stream !=null&& type !=null) {//分享文件Thread t =newThread(newRunnable() {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字符串,没有文件
- finalUri fileUri = creatFileForSharedContent(this, extra_text);//创建文件,将内容写入文件
- if(fileUri !=null) {
- Thread t =newThread(newRunnable() {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 =newIntent(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>
BluetoothOppReceiver 收到此广播后的主要处理代码如下,将此条记录添加到数据库。
- //Inserttransfersessionrecordto databasemOppManager.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,intarrayPos) {if(info.isReadyToStart()) {if(info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {//向外分享、发送
- /* 检查文件是否存在 */}
- }if(mBatchs.size() ==0) {if(info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {//向外分享、发送mTransfer =newBluetoothOppTransfer(this, mPowerManager, newBatch);
- }else if(info.mDirection == BluetoothShare.DIRECTION_INBOUND) {//接收mServerTransfer =newBluetoothOppTransfer(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{//创建BluetoothSocketbtSocket = device.createInsecureRfcommSocketToServiceRecord(BluetoothUuid.ObexObjectPush.getUuid());
- }catch(IOException e1) {//....}try{
- btSocket.connect();//;连接设备BluetoothOppRfcommTransport transport;
- transport =newBluetoothOppRfcommTransport(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(intnumShares) {try{//创建obex clientmCs =newClientSession(mTransport1);
- mConnected =true;
- }catch(IOException e1) {
- }if(mConnected) {
- mConnected =false;
- HeaderSet hs =newHeaderSet();//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() {intstatus = 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= newContentValues();
- 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);}
- }
在之后就是一些状态的处理了。到此通过蓝牙分享文件到流程基本上过了一遍,其中还有许多状态、进度等相关功还没能研究透彻,之后再继续研究。
欢迎扫一扫关注我的微信公众号,定期推送优质技术文章:
来源: http://blog.csdn.net/vnanyesheshou/article/details/70256004