zoukankan      html  css  js  c++  java
  • ZT android -- 蓝牙 bluetooth (四)OPP文件传输

    android -- 蓝牙 bluetooth (四)OPP文件传输

    分类: Android的原生应用分析 2599人阅读 评论(19) 收藏 举报

            在前面android -- 蓝牙 bluetooth (一) 入门文章结尾中提到了会按四个方面来写这系列的文章,前面已写了蓝牙打开和蓝牙搜索,这次一起来看下 蓝牙文件分享的流程,也就是蓝牙应用opp目录下的代码,作为蓝牙最基本的一个功能,这部分的代码在之前的版本中就已经有了,新旧版本代码对比很多类名都 是一样的,这一部分新东西不多,写在这里帮助大家梳理下流程吧。

            有没有这种感觉,智能手机的普及让我们提高了一点对蓝牙的关注,手机间使用蓝牙互传文件应该是最常用的应用之一,手机与电脑也可以通过蓝牙做同样的事情, 大部分笔记本都支持蓝牙功能,本本上蓝牙芯片多数是broadcom的,也有其它厂商(比如东芝)不过数量不多,毕竟broadcom在BT这方面是老 大。不过本本上蓝牙一般只支持蓝牙耳机听歌,并没实现对opp的支持,如果体验下手机与电脑的蓝牙文件传输怎么办呢,安装一个叫bluesoleil(中 文名好像是千月)软件就可以了,这个软件对蓝牙功能的支持还是比较全的。可能需要卸载本本自带蓝牙驱动。扯淡结束,本文还是要关注手机间蓝牙opp的代码 流程,这段的废话也许能帮助你提高下对蓝牙的体验。

            蓝牙发送文件时发送端先来到这里packages/apps/Bluetooth/src/com/android/bluetooth/opp /BluetoothOppLauncherActivity.java,一个没有界面只是提取下文件信息的中转站,源码的注释写的很清楚了,两个分支 action.equals(Intent.ACTION_SEND)和 action.equals(Intent.ACTION_SEND_MULTIPLE)

    1.  if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) {  
    2.             //Check if Bluetooth is available in the beginning instead of at the end  
    3.             if (!isBluetoothAllowed()) {  
    4.                 Intent in = new Intent(this, BluetoothOppBtErrorActivity.class);  
    5.                 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
    6.                 in.putExtra("title"this.getString(R.string.airplane_error_title));  
    7.                 in.putExtra("content"this.getString(R.string.airplane_error_msg));  
    8.                 startActivity(in);  
    9.                 finish();  
    10.                 return;  
    11.             }  
    12.             if (action.equals(Intent.ACTION_SEND)) {  
    13.                .......   
    14.                Thread t = new Thread(new Runnable() {  
    15.                             public void run() {  
    16.                                 BluetoothOppManager.getInstance(BluetoothOppLauncherActivity.this)  
    17.                                     .saveSendingFileInfo(type,fileUri.toString(), false);  
    18.                                 //Done getting file info..Launch device picker  
    19.                                 //and finish this activity  
    20.                                 launchDevicePicker();  
    21.                                 finish();  
    22.                             }  
    23.                         });  ......           
    24.             } else if (action.equals(Intent.ACTION_SEND_MULTIPLE)) {  
    25.               .......                     
    26.             }  

               最前面那个isBluetoothAllowed()会判断是否处于飞行模式,如果是会禁止发送的。在launchDevicePicker()里还会 判断蓝牙是否已经打开,就是下面这个条件语句 (!BluetoothOppManager.getInstance(this).isEnabled())。如果已经打开了蓝牙,如果蓝牙打开了就进 入设备选择界面DeviceListPreferenceFragment(DevicePickerFragment)选择设备,这个跳转过程简单说明 下,注意这个new Intent(BluetoothDevicePicker.ACTION_LAUNCH)里字符串,完整定义public static final String ACTION_LAUNCH = "android.bluetooth.devicepicker.action.LAUNCH";路径frameworks/base/core /java/android/bluetooth/BluetoothDevicePicker.java,你会在setting应用的 manifest.xml里发现

    1. <activity android:name=".bluetooth.DevicePickerActivity"  
    2.                 android:theme="@android:style/Theme.Holo.DialogWhenLarge"  
    3.                 android:label="@string/device_picker"  
    4.                 android:clearTaskOnLaunch="true">  
    5.             <intent-filter>  
    6.                 <action android:name="android.bluetooth.devicepicker.action.LAUNCH" />  
    7.                 <category android:name="android.intent.category.DEFAULT" />  
    8.             </intent-filter>  
    9.         </activity>  
              这样目标就指向了DevicePickerActivity,注意此时它的代码路径是packages/apps/Settings/src/com /android/settings/bluetooth/DevicePickerActivity.java,这个类代码很简单,只有一个 onCreate并只在里加载了一个布局文件bluetooth_device_picker.xml,就是这个布局文件指明下一站在哪,看下面就知道怎 么来到DevicePickerFragment了
    1. <fragment  
    2.         android:id="@+id/bluetooth_device_picker_fragment"  
    3.         android:name="com.android.settings.bluetooth.DevicePickerFragment"  
    4.         android:layout_width="match_parent"  
    5.         android:layout_height="0dip"  
    6.         android:layout_weight="1" />  
              到了这里,已经可看到配对过的蓝牙列表了,选择其中一个点击会来到这里,里面那个sendDevicePickedIntent是我们关心的,又发了一个广播,去找谁收了广播就好了
    1.     void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {  
    2.         mLocalAdapter.stopScanning();  
    3.         LocalBluetoothPreferences.persistSelectedDeviceInPicker(  
    4.                 getActivity(), mSelectedDevice.getAddress());  
    5.         if ((btPreference.getCachedDevice().getBondState() ==  
    6.                 BluetoothDevice.BOND_BONDED) || !mNeedAuth) {  
    7.             sendDevicePickedIntent(mSelectedDevice);  
    8.             finish();  
    9.         } else {  
    10.             super.onDevicePreferenceClick(btPreference);  
    11.         }  
    12.     }<div>    public static final String ACTION_LAUNCH = "android.bluetooth.devicepicker.action.LAUNCH";  
    13.          private void sendDevicePickedIntent(BluetoothDevice device) {  
    14.          Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);  
    15.          intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);  
    16.          if (mLaunchPackage != null && mLaunchClass != null) {  
    17.              intent.setClassName(mLaunchPackage, mLaunchClass);  
    18.          }  
    19.         getActivity().sendBroadcast(intent);}  
    20. </div>  
            通过BluetoothDevicePicker.ACTION_DEVICE_SELECTED查找,会在/packages/apps /Bluetooth/src/com/android/bluetooth/opp/BluetoothOppReceiver.java这个找到对该 广播的处理,也就是下面的代码:
    1. else if (action.equals(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) {  
    2.           BluetoothOppManager mOppManager = BluetoothOppManager.getInstance(context);           
    3.           BluetoothDevice remoteDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);   
    4.            
    5.           // Insert transfer session record to database  
    6.           mOppManager.startTransfer(remoteDevice);  
    7.            
    8.           // Display toast message  
    9.           String deviceName = mOppManager.getDeviceName(remoteDevice);  
    10.           .......  
    11. }  
            看来关键代码是mOppManager.startTransfer(remoteDevice),在packages/apps/Bluetooth /src/com/android/bluetooth/opp/BluetoothOppManager.java,里面开启线程执行发送动作,既然是 开启线程,直接去看run方法就是了,方法里面依旧区分单个和多个文件的发送,看一个就可以。
    1.    public void startTransfer(BluetoothDevice device) {  
    2.         if (V) Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum);  
    3.         InsertShareInfoThread insertThread;  
    4.         synchronized (BluetoothOppManager.this) {  
    5.             if (mInsertShareThreadNum > ALLOWED_INSERT_SHARE_THREAD_NUMBER) {  
    6.                 ...........  
    7.                 return;  
    8.             }  
    9.             insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendingFile,  
    10.                     mUriOfSendingFile, mMimeTypeOfSendingFiles, mUrisOfSendingFiles,  
    11.                     mIsHandoverInitiated);  
    12.             if (mMultipleFlag) {  
    13.                 mfileNumInBatch = mUrisOfSendingFiles.size();  
    14.             }  
    15.         }  
    16.         insertThread.start();  
    17.     }             
    18.     public void run() {  
    19.             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
    20.             ..........  
    21.             if (mIsMultiple) {  
    22.                 insertMultipleShare();  
    23.             } else {  
    24.                 insertSingleShare();  
    25.             }  
    26.             .......... }  

           以insertSingleShare() 为例,在它的实现会看到mContext.getContentResolver().insert,不多想了,要去provider里找到insert()函数了,

    对应的代码在BluetoothOppProvider.java (bluetoothsrccomandroidluetoothopp),insert的函数实现如下,里面又拉起 BluetoothOppService,开始还以为只是针对数据库的操作,差点错过了风景。路径/packages/apps/Bluetooth /src/com/android/bluetooth/opp/BluetoothOppService.java

    1. public Uri insert(Uri uri, ContentValues values) {  
    2.  if (rowID != -1) {  
    3.      context.startService(new Intent(context, BluetoothOppService.class));  
    4.       ret = Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID);  
    5.      context.getContentResolver().notifyChange(uri, null);  
    6.  } else {  
    7.       if (D) Log.d(TAG, "couldn't insert into btopp database");  
    8.  }  

           在BluetoothOppService的onStartCommand方法中会看到updateFromProvider(),这里又开启了一个线 程UpdateThread,后续代码当然是看它的run方法了,这里面内容不少,好在这部分代码注释比较多,理解起来不难。先暂时只关心发送的动作 insertShare方法,代码也不少,只贴出了告诉我们接下来去哪里的代码和有关的逻辑注释,在下面的代码我们可以看 到 BluetoothOppTransfer.java的对象,下一站就是它了。

    1. private void insertShare(Cursor cursor, int arrayPos) {  
    2.     .........  
    3.     /* 
    4.      * Add info into a batch. The logic is 
    5.      * 1) Only add valid and readyToStart info 
    6.      * 2) If there is no batch, create a batch and insert this transfer into batch, 
    7.      * then run the batch 
    8.      * 3) If there is existing batch and timestamp match, insert transfer into batch 
    9.      * 4) If there is existing batch and timestamp does not match, create a new batch and 
    10.      * put in queue 
    11.      */  
    12.     if (info.isReadyToStart()) {  
    13.         .............  
    14.         if (mBatchs.size() == 0) {  
    15.            ........  
    16.             mBatchs.add(newBatch);  
    17.             if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {  
    18.                  mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);  
    19.             } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {  
    20.                 mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,  
    21.                         mServerSession);  
    22.             }  
    23.   
    24.             if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {  
    25.                 mTransfer.start();  
    26.             } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND  
    27.                     && mServerTransfer != null) {  
    28.                 mServerTransfer.start();  
    29.             }  
    30.   
    31.         } else {  
    32.             .........  
    33.     }}  
            虽然名字是start(),可实际并不是什么线程的,就是一普通方法的,路径是/packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
    1. public void start() {  
    2.       ....这里省略未贴的代码是检查蓝牙是否打开,一个很谨慎的判断。看似无用,不过还是安全第一。  
    3.   
    4.       if (mHandlerThread == null) {  
    5.           ........  
    6.           if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {  
    7.               /* for outbound transfer, we do connect first */  
    8.               startConnectSession();  
    9.           } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {  
    10.               /* 
    11.                * for inbound transfer, it's already connected, so we start 
    12.                * OBEX session directly 
    13.                */  
    14.               startObexSession();  
    15.           }  
    16.       }  
    17.   }  

            上面的代码是分发送文件和接收文件的,看下这两行代码就很清楚了,如果分享给别人是OUTBOUND,先执行 startConnectSession(),这个函数最后还是要跑到startObexSession()这里的,如果收文件直接 startObexSession,所以后面就只看startObexSession方法了

    1. // This transfer is outbound, e.g. share file to other device.  
    2.  public static final int DIRECTION_OUTBOUND = 0;  
    3.  // This transfer is inbound, e.g. receive file from other device.  
    4.  public static final int DIRECTION_INBOUND = 1;  
           还是在同一个类里,发送流程快结束了,同样区分是传入还是传出,发文件看OUTBOUND,去BluetoothOppObexClientSession.java
    1. private void startObexSession() {       
    2.       if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {  
    3.           if (V) Log.v(TAG, "Create Client session with transport " + mTransport.toString());  
    4.           mSession = new BluetoothOppObexClientSession(mContext, mTransport);  
    5.       } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {           
    6.           if (mSession == null) {  
    7.                markBatchFailed();  
    8.               mBatch.mStatus = Constants.BATCH_STATUS_FAILED;  
    9.               return;  
    10.           }  
    11.           if (V) Log.v(TAG, "Transfer has Server session" + mSession.toString());  
    12.       }  
    13.       mSession.start(mSessionHandler);  
    14.       processCurrentShare();  
    15.   }  
           同样名字是start,实际只是一个普通方法而已,会看又是一个线程 mThread = new ClientThread(mContext, mTransport),这时的start才是线程的start(),还是看run方法,一些线程状态的判断,看到doSend() 就是了,直正的发送在这里packages/apps/Bluetooth/src/com/android/bluetooth/opp /BluetoothOppObexClientSession.java,
    1. private void doSend() {  
    2.   
    3.      int status = BluetoothShare.STATUS_SUCCESS;  
    4.      ........关于status值的判断  
    5.      if (status == BluetoothShare.STATUS_SUCCESS) {  
    6.          /* do real send */ //看到这个注释了没,它才是真家伙sendFile  
    7.          if (mFileInfo.mFileName != null) {  
    8.              status = sendFile(mFileInfo);  
    9.          } else {  
    10.              /* this is invalid request */  
    11.              status = mFileInfo.mStatus;  
    12.          }  
    13.          waitingForShare = true;  
    14.      } else {  
    15.          Constants.updateShareStatus(mContext1, mInfo.mId, status);  
    16.      }  
    17.   
    18.      if (status == BluetoothShare.STATUS_SUCCESS) {  
    19.          Message msg = Message.obtain(mCallback);  
    20.          msg.what = BluetoothOppObexSession.MSG_SHARE_COMPLETE;  
    21.          msg.obj = mInfo;  
    22.          msg.sendToTarget();  
    23.      } else {  
    24.          Message msg = Message.obtain(mCallback);  
    25.          msg.what = BluetoothOppObexSession.MSG_SESSION_ERROR;  
    26.          mInfo.mStatus = status;  
    27.          msg.obj = mInfo;  
    28.          msg.sendToTarget();  
    29.      }  
    30.  }  
            sendFile是真正干活的,执行完sendFile会把分享成功或失败的消息传回去,sendFile里会执行打包的过程,对于字段的含义要看Headset.java,

    代码路径在frameworks/base/obex/javax/obex/HeaderSet.java。这个sendFile方法行数虽然 多,不过逻辑还是比较清晰的,在这里就不贴了。到这蓝牙发送文件流程也就此结束。由于发送文件时长肯定是不确定,所以在这个流程我们看到了很多开启线程代 码也是很正常的,对于这线程,直接看对应的run方法就是了。

            对于蓝牙接收文件时会收到MSG_INCOMING_BTOPP_CONNECTION消息,收到这个消息是由于在蓝牙打开,即蓝牙状态是 BluetoothAdapter.STATE_ON时会执行

    startSocketListener(),在这个函数开启了监听程序,看下面贴在一起的代码就明白了,

    1. if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {  
    2.     switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {  
    3.         case BluetoothAdapter.STATE_ON:  
    4.             if (V) Log.v(TAG,"Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");  
    5.             startSocketListener();  
    6.             break;  
    7.                           
    8. private void startSocketListener() {  
    9.         if (V) Log.v(TAG, "start RfcommListener");  
    10.         mSocketListener.start(mHandler);  
    11.         if (V) Log.v(TAG, "RfcommListener started");  
    12. }  
    13. mSocketListener.start(mHandler);这个的实现在这里,比较长,没有贴上来     
    14. /packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppRfcommListener.java  
            回到上面处理消息,在BluetoothOppService.java的handlemessage中这个分支 case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION, 创建一个 createServerSession(transport); 最后走/frameworks/base/obex/javax/obex/ServerSession.java的run方法中接收数据
    1. private void createServerSession(ObexTransport transport) {  
    2.      mServerSession = new BluetoothOppObexServerSession(this, transport);  
    3.      mServerSession.preStart();       
    4.  }  
            对于蓝牙接收文件部分的流程还没有细致的跟踪,暂时只看到这里,对于了解基本流程这此应该够用了,同时如果想更好理解蓝牙OPP文件传输,了解是OBEX 基础协议也是有必要的,网上资料还是有不少的,多数是论文形式的。对于蓝牙OPP部分,本文只是描述android代码中的流程,旨在帮你快速的理清流 程,本文对OPP本身并没有深入,相关的知识需要进一步学习才行,有同道先行的童鞋还望赐教一二,谢谢。


    更多 0
     
    查看评论
    9楼 syngalon螳螳 2013-11-05 09:58发表 [回复]
    请问蓝牙手机接收文件后存储路径在哪里设置呢
    Re: balmy 2013-11-05 12:01发表 [回复]
    回复hypersmart:默认好像不能手动设置吧,在代码里写好了,应该可以在下面的代码里改,要试验下确认
    packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppReceiveFileInfo.java
    generateFileInfo方法
    packages/apps/Bluetooth/src/com/android/bluetooth/opp/Constants.java
    public static final String DEFAULT_STORE_SUBDIR = "/bluetooth";
    Re: syngalon螳螳 2013-11-11 08:55发表 [回复]
    回复baimy1985:谢谢呀,就是想找到定义蓝牙文件存储位置的地方,原来在这里
    8楼 syngalon螳螳 2013-10-24 16:06发表 [回复]
    请问一下,台式机接usb蓝牙适配器,然后用bluesoleil怎么连接不了手机呢,需要什么配置吗-
    我是之前坐你后面的 *航
    Re: balmy 2013-10-24 19:56发表 [回复]
    回复hypersmart:台式机一般是需要额外蓝牙发射器的,有的笔记本可能自带了就不用了,好像还要处理下驱动,如果之前有默认的蓝牙驱动,还要卸载掉相冲突的蓝牙驱动才可以
    Re: syngalon螳螳 2013-10-25 15:33发表 [回复]
    回 复baimy1985:非常感谢,蓝牙发射器不是插在台式机上的蓝牙适配器么,已经插上了,不用千月还能找到蓝牙手机,装上就发现不了手机,卸载后也能发 现手机,很奇怪,设备管理器里面有三个“bluetooth外围设备”上打了感叹号,安装了蓝牙驱动(Widcomm.v5.0.1.801)也不行,有 木有其他的蓝牙客户端呀,想看看ftp传输文件功能
    Re: balmy 2013-10-25 22:02发表 [回复]
    回复hypersmart:其它的我也不知道,这个你可以问下Broadcom的FAE,他们应该了解多一些
    Re: syngalon螳螳 2013-10-26 14:12发表 [回复]
    回复baimy1985:恩恩,我问问,谢谢!
    7楼 gordon1986 2013-09-30 10:12发表 [回复]
    不错,支持一个
    6楼 _小强_ 2013-09-09 17:40发表 [回复]
    楼主,有没有关于蓝牙设置、连接、文件传输的分析文档!
    5楼 xyp5299 2013-08-20 14:41发表 [回复]
    wow,非常有用。谢谢。请继续分享。
    4楼 司夜刺客 2013-08-09 09:42发表 [回复]
    LZ分析的不错,基本上OPP在JAVA层的东西都理清除了,如果能够加上Jni和bluedroid协议栈部分的分析就更好啦!嘿嘿
    Re: balmy 2013-08-09 21:08发表 [回复]
    回复goahead111:呵呵,协议方面的还不知道如何下手呢,开发多数时间用JAVA,所以JAVA这部分看到相对快一些。
    3楼 imwhite 2013-08-05 13:58发表 [回复]
    博主研究得还挺深入的。不过我有个疑问:
    程序可以利用调用系统蓝牙的Provider来实现发送和接受文件,但是程序是如何得知发送或者接受文件的状况呢?
    如果是通过Receiver,那么注册的action是什么?
    Re: balmy 2013-08-05 21:38发表 [回复]
    回复q376420785:你看看这个函数updateActiveNotification() 是不是你想要的
    路径:
    /packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppNotification.java
    Re: imwhite 2013-08-06 18:18发表 [回复]
    回复baimy1985:我发现最后还是调用INotificationManager的enqueueNotificationWithTag方法,不能跟踪下去了。
    我还遇到一个问题:
    我 想调用蓝牙的opp传输文件,于是就使用intent的ACTION_SEND共享方式来打开,但是会出现选择设备界面(Bluetooth device picker),但是我想自己设计设备列表,然后点击设备就可以发送文件,不知道怎样才能不显示系统的选择界面而直接发送文件呢?
    Re: balmy 2013-08-06 21:15发表 [回复]
    回复q376420785:自己设计列表应该是可以的,只是如果这样应该就不能用ACTION_SEND的了,相当于自定义一个列表选项,列表的内容你是知道的,具体每个选项的要做什么要清楚,至于发送细节应该没区别的。
    2楼 frogoscar 2013-07-24 13:44发表 [回复]
    超有用!!!!!!
    1楼 frogoscar 2013-07-17 22:57发表 [回复]
    大牛!!
  • 相关阅读:
    68
    56
    Django manager 命令笔记
    Django 执行 manage 命令方式
    Django 连接 Mysql (8.0.16) 失败
    Python django 安装 mysqlclient 失败
    H.264 SODB RBSP EBSP的区别
    FFmpeg—— Bitstream Filters 作用
    MySQL 远程连接问题 (Windows Server)
    MySQL 笔记
  • 原文地址:https://www.cnblogs.com/jeanschen/p/3528256.html
Copyright © 2011-2022 走看看