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

时间:2023-03-08 22:27:49
ZT android -- 蓝牙 bluetooth (四)OPP文件传输

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

分类: Android的原生应用分析 2013-06-22 21:51 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. // Insert transfer session record to database
  5. mOppManager.startTransfer(remoteDevice);
  6. // Display toast message
  7. String deviceName = mOppManager.getDeviceName(remoteDevice);
  8. .......
  9. }

看来关键代码是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
(bluetooth\src\com\android\bluetooth\opp),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. if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
  24. mTransfer.start();
  25. } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
  26. && mServerTransfer != null) {
  27. mServerTransfer.start();
  28. }
  29. } else {
  30. .........
  31. }}

虽然名字是start(),可实际并不是什么线程的,就是一普通方法的,路径是/packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppTransfer.java

  1. public void start() {
  2. ....这里省略未贴的代码是检查蓝牙是否打开,一个很谨慎的判断。看似无用,不过还是安全第一。
  3. if (mHandlerThread == null) {
  4. ........
  5. if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
  6. /* for outbound transfer, we do connect first */
  7. startConnectSession();
  8. } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
  9. /*
  10. * for inbound transfer, it's already connected, so we start
  11. * OBEX session directly
  12. */
  13. startObexSession();
  14. }
  15. }
  16. }

上面的代码是分发送文件和接收文件的,看下这两行代码就很清楚了,如果分享给别人是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. int status = BluetoothShare.STATUS_SUCCESS;
  3. ........关于status值的判断
  4. if (status == BluetoothShare.STATUS_SUCCESS) {
  5. /* do real send */ //看到这个注释了没,它才是真家伙sendFile
  6. if (mFileInfo.mFileName != null) {
  7. status = sendFile(mFileInfo);
  8. } else {
  9. /* this is invalid request */
  10. status = mFileInfo.mStatus;
  11. }
  12. waitingForShare = true;
  13. } else {
  14. Constants.updateShareStatus(mContext1, mInfo.mId, status);
  15. }
  16. if (status == BluetoothShare.STATUS_SUCCESS) {
  17. Message msg = Message.obtain(mCallback);
  18. msg.what = BluetoothOppObexSession.MSG_SHARE_COMPLETE;
  19. msg.obj = mInfo;
  20. msg.sendToTarget();
  21. } else {
  22. Message msg = Message.obtain(mCallback);
  23. msg.what = BluetoothOppObexSession.MSG_SESSION_ERROR;
  24. mInfo.mStatus = status;
  25. msg.obj = mInfo;
  26. msg.sendToTarget();
  27. }
  28. }

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. private void startSocketListener() {
  8. if (V) Log.v(TAG, "start RfcommListener");
  9. mSocketListener.start(mHandler);
  10. if (V) Log.v(TAG, "RfcommListener started");
  11. }
  12. mSocketListener.start(mHandler);这个的实现在这里,比较长,没有贴上来
  13. /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发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
请问蓝牙手机接收文件后存储路径在哪里设置呢
Re: balmy 2013-11-05 12:01发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复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发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复baimy1985:谢谢呀,就是想找到定义蓝牙文件存储位置的地方,原来在这里
8楼 syngalon螳螳 2013-10-24 16:06发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
请问一下,台式机接usb蓝牙适配器,然后用bluesoleil怎么连接不了手机呢,需要什么配置吗-
我是之前坐你后面的 *航
Re: balmy 2013-10-24 19:56发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复hypersmart:台式机一般是需要额外蓝牙发射器的,有的笔记本可能自带了就不用了,好像还要处理下驱动,如果之前有默认的蓝牙驱动,还要卸载掉相冲突的蓝牙驱动才可以
Re: syngalon螳螳 2013-10-25 15:33发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输

复baimy1985:非常感谢,蓝牙发射器不是插在台式机上的蓝牙适配器么,已经插上了,不用千月还能找到蓝牙手机,装上就发现不了手机,卸载后也能发
现手机,很奇怪,设备管理器里面有三个“bluetooth外围设备”上打了感叹号,安装了蓝牙驱动(Widcomm.v5.0.1.801)也不行,有
木有其他的蓝牙客户端呀,想看看ftp传输文件功能
Re: balmy 2013-10-25 22:02发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复hypersmart:其它的我也不知道,这个你可以问下Broadcom的FAE,他们应该了解多一些
Re: syngalon螳螳 2013-10-26 14:12发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复baimy1985:恩恩,我问问,谢谢!
7楼 gordon1986 2013-09-30 10:12发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
不错,支持一个
6楼 _小强_ 2013-09-09 17:40发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
楼主,有没有关于蓝牙设置、连接、文件传输的分析文档!
5楼 xyp5299 2013-08-20 14:41发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
wow,非常有用。谢谢。请继续分享。
4楼 司夜刺客 2013-08-09 09:42发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
LZ分析的不错,基本上OPP在JAVA层的东西都理清除了,如果能够加上Jni和bluedroid协议栈部分的分析就更好啦!嘿嘿
Re: balmy 2013-08-09 21:08发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复goahead111:呵呵,协议方面的还不知道如何下手呢,开发多数时间用JAVA,所以JAVA这部分看到相对快一些。
3楼 imwhite 2013-08-05 13:58发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
博主研究得还挺深入的。不过我有个疑问:
程序可以利用调用系统蓝牙的Provider来实现发送和接受文件,但是程序是如何得知发送或者接受文件的状况呢?
如果是通过Receiver,那么注册的action是什么?
Re: balmy 2013-08-05 21:38发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复q376420785:你看看这个函数updateActiveNotification() 是不是你想要的
路径:
/packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppNotification.java
Re: imwhite 2013-08-06 18:18发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复baimy1985:我发现最后还是调用INotificationManager的enqueueNotificationWithTag方法,不能跟踪下去了。
我还遇到一个问题:

想调用蓝牙的opp传输文件,于是就使用intent的ACTION_SEND共享方式来打开,但是会出现选择设备界面(Bluetooth
device picker),但是我想自己设计设备列表,然后点击设备就可以发送文件,不知道怎样才能不显示系统的选择界面而直接发送文件呢?
Re: balmy 2013-08-06 21:15发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
回复q376420785:自己设计列表应该是可以的,只是如果这样应该就不能用ACTION_SEND的了,相当于自定义一个列表选项,列表的内容你是知道的,具体每个选项的要做什么要清楚,至于发送细节应该没区别的。
2楼 frogoscar 2013-07-24 13:44发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
超有用!!!!!!
1楼 frogoscar 2013-07-17 22:57发表 [回复]
ZT android -- 蓝牙 bluetooth (四)OPP文件传输
大牛!!