带你解锁蓝牙skill(三)

蓝牙这块儿算是系统中的一个大块儿,刚开始分析确实很容易没有头绪,所以在进入庞大的源码之前先确定一个分析顺序,也好避免越学越乱。 对于源码的分析不外乎whw(what—how—why)

对于蓝牙各协议的功能以及如何演示都已经分析完了,具体可以参考 带你解锁蓝牙skill(一)以及带你解锁蓝牙skill(二)

本文以Android7.0为例进行源码分析。开始分析源码之前,先来看看蓝牙相关的都有什么东西

4,如何开始

在对一个新事物进行研究之前,我们已经了解了他是什么,那么接下来就是庖丁解牛了。但是目前还做不到目无全牛哈哈。 蓝牙代码实现不外乎包括以下三个方面

  1. 界面UI
    • 设置应用中蓝牙的ui
    • 蓝牙本身这个系统应用中的ui
  2. 蓝牙开关默认值
  3. 协议配置开关:手机是否要支持各种协议

在学之前我们也先要明确目的是什么,即学完蓝牙后我们想要掌握什么样的技能?? 大致如下:

  1. 对于系统开发工作者
    • 掌握基础界面修改及相关逻辑
    • 掌握手机蓝牙开关的系统默认值
    • 掌握蓝牙各协议开关(即禁用/启用蓝牙协议)
    • 掌握如何新增蓝牙协议(更高要求)
  2. 对于应用开发工作者
    • 掌握蓝牙的基本用法:包括开关,扫描,配对,连接等等(毕竟系统应用你并不能决定,你只能是调用各接口)
    • 掌握各蓝牙协议的基本用法(比如实现一个读取联系人的操作)

    在确定了研究思路和研究目的之后,我们就可以开始对源码的研究了。

5,蓝牙源码研究

按照第四部分确定的大致方向来进行接下来的研究。不论是蓝牙开关默认值还是协议的开关的值,对这些值还好说,三下五除二就分析好了,但是蓝牙界面仍旧有一个大工程在。从驱动往应用层方向,蓝牙相关的代码位置如下

  1. 第一部分,系统应用设置Settings中的蓝牙相关,显示从设置进入蓝牙的一些界面,代码位置为:packages/apps/Settings/src/com/android/settings/bluetooth/目录结构如下:包括蓝牙开关,蓝牙扫描,蓝牙配对框,蓝牙重命名框,蓝牙选择框等等
  1. 第二部分,系统中有个蓝牙应用Bluetooth,显示界面诸如蓝牙文件传入传出历史记录,蓝牙配对框,蓝牙文件传输框等等,代码位置为:/packages/apps/Bluetooth,Java代码目录结构如下:

可以看出Bluetooth应用中的代码是按照各协议模块进行区分目录的,但是在各协议模块中并不包含对profile的具体定义,以A2DP为例

在packages/apps/Bluetooth/src/com/anddroid/bluetooth/a2dp文件夹中只有两个文件A2dpService和A2dpStateMachine,至于这两个文件是干什么,后续会介绍,暂时先了解一个大致的目录结构

  1. 第三部分,就是蓝牙协议的具体实现,代码所在路径:/frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/

在该目录下就是一些蓝牙profile的相关的配置了。

  1. 第四部分,集成的一些蓝牙接口:frameworks/base/core/java/android/bluetooth/,目录结构如下:

可以看到很多在开发过程中常见的类:BluetoothAdapter,BluetoothDevice,BluetoothSocket等等,蓝牙的核心代码和接口的具体实现都在这里!!!

  1. 第五部分,手机开机后启动的蓝牙服务是BluetoothManagerService,代码目录结构为:/frameworks/base/services/core/java/com/android/server/

对蓝牙的应用层的代码接口有了一个大致了解之后,我们开始进行分析

1>,蓝牙界面相关

估计有着急的人会说,看什么界面啊,我就想知道功能是怎么实现的 但我想说,如果没有界面,你如何知道他到底实现了什么功能?? 如果没有界面,你该如何下手??

界面大致包括两部分,设置中的蓝牙界面和蓝牙应用中的蓝牙界面

  • 设置中蓝牙相关的界面 蓝牙界面相关的分析在去年做过,虽然是4.4.2但是原理还是一样的,也没必要再做那么多无用功,贴上博客链接 蓝牙界面实现分析(一) 蓝牙界面实现分析(二) 蓝牙界面实现分析(三) 有需要的可以看一下 在这里需要说明一下:关于已配对设备的各种配置协议显示界面 蓝牙设备间的连接其实就是各协议之间的连接,蓝牙设备间有可能有多个协议连接,但每个协议仅支持一个连接 在设备进行配对时,发送设备配对状态发生改变的广播,在监听到广播后设备会去获取到remote设备所支持的协议,并尝试进行连接,比如如果remote设备支持手机音频或者媒体音频,那么本机设备就会尝试该协议的连接。但如果本机设备已经与另一台B远程设备进行了手机音频的连接,那么本机设备会先断开与B设备的连接,去尝试和remote设备进行手机音频的连接。 也就是说某一个时刻设备某个协议连接只能支持一个远程设备,但是可以支持多个远程设备进行多个协议连接。 设备A在某时刻可以和远程设备B进行媒体音频连接同时保持和远程设备C之间的手机音频连接,如果设备A需要播放媒体音频(比如音乐,视频等)音频会在设备B中进行播放,如果设备A需要进行播放手机音频(即手机通话)会在设备C中进行播放。
  • 蓝牙应用相关界面 蓝牙应用是为了配合各蓝牙协议而存在的,所以,针对蓝牙应用相关界面会穿插在蓝牙协议的分析中 蓝牙配对弹出框过程分析讲述了蓝牙配对弹出框的原理。
  • 蓝牙协议开关

蓝牙协议开关这篇文章中讲述了作为系统开发人员如何禁止掉蓝牙某个协议(包括上层和底层)

2>,蓝牙协议实现

对于蓝牙协议我只能是分析常见且我的测试机可以实现的,计划要分析的协议如下

  • opp文件传输协议
  • 互联网协议
  • Hsp手机音频协议
  • A2dp媒体音频协议

也许后续计划会有所改变,先暂时确定这样。

i>,opp协议

以蓝牙传输图片为例,opp文件传输包括文件的传入和传出两方面,分别来考虑

  • 传出文件
  • 传入文件

传出文件

我们就沿着分享图片这一条线去分析,在分享蓝牙图片时,选择蓝牙分享,当然如果蓝牙未开启的话会询问先要开启蓝牙。至于系统分享属于另一个系统的功能,在以后的文章中会介绍。 先介绍一个目前用到的,在选择蓝牙分享后,会启动BluetoothOppLauncherActivity,在该类中的launchDevicePicker()启动DevicePickerActivity,方法如下:

    /**
    如果蓝牙未开启就开启蓝牙,如果蓝牙已开启就启动选择蓝牙设备界面
     * Turns on Bluetooth if not already on, or launches device picker if Bluetooth is on
     * @return
     */
private final void launchDevicePicker() {
        // TODO: In the future, we may send intent to DevicePickerActivity
        // directly,
        // and let DevicePickerActivity to handle Bluetooth Enable.
        if (!BluetoothOppManager.getInstance(this).isEnabled()) {
            if (V) Log.v(TAG, "Prepare Enable BT!! ");
            //如果蓝牙未开启,就去开启蓝牙
            Intent in = new Intent(this, BluetoothOppBtEnableActivity.class);
            in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(in);
        } else {
        //如果蓝牙已开启就启动DevicePickerActiivty,会传入一些参数,这个在以后会用到
            if (V) Log.v(TAG, "BT already enabled!! ");
            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());
            if (V) Log.d(TAG,"Launching "+BluetoothDevicePicker.ACTION_LAUNCH);
            startActivity(in1);
        }
    }

开启蓝牙之后,弹出选择蓝牙设备界面

那么该界面显示的蓝牙设备都包括什么呢?以及点击蓝牙设备后又会去做什么事儿呢?带着这些问题来继续接下来的分析

首先我们要知道该界面所加载的activity的名字,这个可以借助sdk的工具很明显的看出。在这里说明一下,sdk中有很多工具可以方便我们的分析,就在sdk\tools目录下,大家可以自己去尝试。

可以看到该在选择蓝牙分享后弹出的activity的界面为DevicePickerActiviy.java。借助谷歌源码网址AndroidXRef可以快速找到该java文件,进行分析。

该activity的所在目录如下:位于settings应用中(代码来自Android7.0.0_r1分支)

出乎意料。代码简直少的不能再少了

/**
*该activity是蓝牙设备选择时的dialog(这里说是dialog的意思
是该activity的主题是dialog形式的),设备选择的
逻辑实现在BluetoothSettings的fragment中
 * Activity for Bluetooth device picker dialog. The device picker logic
 * is implemented in the {@link BluetoothSettings} fragment.
 */
public final class DevicePickerActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.bluetooth_device_picker);
    }
}

看到这里你可能也许还会困惑,那这个界面怎么加载出来的?逻辑实现呢?人家说的很清楚了,设备选择的逻辑代码在fragment中,而且还给你指明了和BluetoothSettings相关,也就是说具体的你去BluetoothSettings中找去吧。 但是我们先不着急看BluetoothSettings,先看一下DevicePickerActivity中的代码。该类中就有一个需要分析的,那就是他的布局文件bluetooth_device_picker.xml,该文件内容也是很少

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/bluetooth_device_picker_fragment"
        android:name="com.android.settings.bluetooth.DevicePickerFragment"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:layout_weight="1" />
</LinearLayout>

看到这里,差不多就明白了,原来该activity是加载了一个fragment:DevicePickerFragment。是不是差一点就错过了呢。好了,接下来就是去分析该fragment了。 每次去分析一个文件时,首先要明白你想从该文件中明白什么?然后在分析结束后再看看你原先的疑问有没有解决,以及你有什么新的疑问。 所以,明确分析目的:

  • 会加载什么样的设备
  • 设备是如何加载的
  • 设备的点击事件的处理

在继续接下来的阅读时我假设你是对settings源码已经有了一定的了解,如果没有建议你先看看我对源码的settings或者蓝牙的一些基础界面的分析。不论是哪个源码版本,大致都是相通的。 话不多说,进入正题,先来看看DevicePickerFragment类

/**
这句不用翻译了吧各位,,BluetoothSettings是在设置应用中蓝牙的配置和连接管理的界面
 * BluetoothSettings is the Settings screen for Bluetooth configuration and
 * connection management.
 */
public final class DevicePickerFragment extends DeviceListPreferenceFragment {

    public DevicePickerFragment() {
        super(null /* Not tied to any user restrictions. */);
    }

    private boolean mNeedAuth;
    private String mLaunchPackage;
    private String mLaunchClass;
    private boolean mStartScanOnResume;
    private ListView mListView;//zhaohaiyun add
     private TextView mEmptyView;//zhaohaiyun add
    @Override
    void addPreferencesForActivity() {
        addPreferencesFromResource(R.xml.device_picker);

        Intent intent = getActivity().getIntent();
        mNeedAuth = intent.getBooleanExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);

      //调用父类方法设置过滤器,过滤蓝牙设备  setFilter(intent.getIntExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
                BluetoothDevicePicker.FILTER_TYPE_ALL));
                //mLaunchPackage 的取值为Constants.THIS_PACKAGE_NAME
                //即取值为com.android.bluetooth
        mLaunchPackage = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE);
        //mLaunchClass 取值为BluetoothOppReceiver.class.getName()
        //即要启动的class为BluetoothOppReceiver
        mLaunchClass = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //设置界面的标题
        getActivity().setTitle(getString(R.string.device_picker));
        UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
        mStartScanOnResume = !um.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH)
                && (savedInstanceState == null);  // don't start scan after rotation,在进行横竖屏切换时数据会保存,不要重新扫描
    }

    @Override
    public void onResume() {
        super.onResume();
        //添加扫描到的蓝牙设备
        addCachedDevices();
        if (mStartScanOnResume) {
        //如果满足扫描条件,则进行蓝牙扫描
            mLocalAdapter.startScanning(true);
            mStartScanOnResume = false;
        }
    }

    @Override
    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);
        }
    }

    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice,
            int bondState) {
        if (bondState == BluetoothDevice.BOND_BONDED) {
            BluetoothDevice device = cachedDevice.getDevice();
            if (device.equals(mSelectedDevice)) {
                sendDevicePickedIntent(device);
                finish();
            }
        }
    }

    @Override
    public void onBluetoothStateChanged(int bluetoothState) {
        super.onBluetoothStateChanged(bluetoothState);

//如果蓝牙状态发生改变,且目前属于开启状态时也会进行扫描
        if (bluetoothState == BluetoothAdapter.STATE_ON) {
            mLocalAdapter.startScanning(false);
        }
    }

    private void sendDevicePickedIntent(BluetoothDevice device) {
        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);
    }
}
  • 首先我们看到了两个问题,包括什么设备,以及如何添加的?
    • 在onResume方法中调用addCachedDevices方法添加蓝牙设备。因为所设备的过滤器是FILTER_TYPE_ALL,所以所添加的设备包括已配对的,已连接的,以及附近可用设备即所有蓝牙设备 针对选择蓝牙设备界面的filter共有以下几种取值(为什么我这么说,因为对于其他界面比如BLuetoothSettings时filter又会有别的取值,在加载已配对设备和可用设备时区分是靠BluetoothDeviceFilter中的取值)

    以上是BluetoothDevicePicker中的一个代码片段,可以看出filter的类型包括

    • FILTER_TYPE_ALL:没有什么限制条件,显示所有蓝牙设备
    • FILTER_TYPE_AUDIO:只显示支持audio协议的蓝牙设备
    • FILTER_TYPE_TRANSFER:只显示支持文件传输的蓝牙设备
    • FILTER_TYPE_PANU:只显示支持个人局域网用户即可以使用个人局域网的蓝牙设备
    • FILTER_TYPE_NAP:只显示支持开启个人局域网的蓝牙设备

    所以我们说该界面是加载说有类型的蓝牙设备。

    • 说明一下,在蓝牙扫描到设备后会缓存起来,通过addCachedDevices方法,就算不经过扫描也可以获取到曾经缓存起来的蓝牙设备。
    • 在开启该activity时,如果满足扫描条件的话,也会进行蓝牙扫描。
    • 在蓝牙状态发生改变并且当前蓝牙状态为开启时也会进行扫描
  • 设备的点击事件? 设备点击事件onDevicePreferenceClick中处理,针对设备点击事件有两个分支
    • 第一种情况,远程设备已经与本机设备配对或者是远程设备在接收文件时不需要授权即不需要配对的

    这种情况下,会调用sendDevicePickedIntent确认已经选择成功,并结束当前界面

    • 第二种情况,远程设备未与本机设备配对,并且远程设备在接收文件时需要授权的。

    在点击时,当前界面不会结束,会先去调用父类的方法进行配对,配对成功后发送广播,触发该类中的onDeviceBondStateChanged方法,在该方法中检测到所配对的设备就是所选择的设备后重复第一种情况的行为

    所以总结下就是,在点击选择蓝牙设备时,如果设备已和本机设备完成配对,则finish掉该activity并且调用sendDevicePickerIntent方法。如果设备不需要授权即不需要配对就可以发送文件则也是调用sendDevicePickerIntent。如果设备需要授权且未配对的情况下就会去调用父类的方法进行配对操作,配对成功后依旧是调用sendDevicePickerIntent方法。所以就是在保证设备可以接受文件后就调用sendDevicePickerIntent方法。接下来看该方法的具体实现

 private void sendDevicePickedIntent(BluetoothDevice device) {
        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);
    }

在该方法中会发送一个广播。所发送的广播的action为ACTION_DEVICE_SELECTED,携带的字段有EXTRA_DEVICE。LaunchPackage和launchclass是在创建activity时所携带过来的信息,具体参考DevicePickerActivity代码分析注释。归根结底,该方法就是去启动BlueoothOppReceiver。 好了,现在DevicePickerActivity这个界面和文件都分析完了,接下来要进去下一个界面文件BluetoothOppReceive分析了。

在选择蓝牙设备后发送的广播为BluetoothDevicePicker.ACTION_DEVICE_SELECTED,所以看receiver中个对于该广播的处理

/**
用于处理蓝牙文件传输:包括系统广播,来自其他应用的intents,来自OppService的Intents,来自Opp应用层其他模块的Intents
 * Receives and handles: system broadcasts; Intents from other applications;
 * Intents from OppService; Intents from modules in Opp application layer.
 */
public class BluetoothOppReceiver extends BroadcastReceiver {
.....
//接受到向其他蓝牙设备发送文件的广播
 if (action.equals(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) {
            BluetoothOppManager mOppManager = BluetoothOppManager.getInstance(context);
            //获取到远程蓝牙设备信息,即获取到文件接收者
            BluetoothDevice remoteDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            if (V) Log.v(TAG, "Received BT device selected intent, bt device: " + remoteDevice);

            // Insert transfer session record to database
            //开始传输文件。将要分享的文件插入到db中
            mOppManager.startTransfer(remoteDevice);

            // Display toast message
            String deviceName = remoteDevice.getName();
            String toastMsg;
            //传输的文件数量
            int batchSize = mOppManager.getBatchSize();
            if (mOppManager.mMultipleFlag) {
            //如果是发送多个文件,获取对应toast信息
                toastMsg = context.getString(R.string.bt_toast_5, Integer.toString(batchSize),
                        deviceName);
            } else {
            //如果是发送单个文件,获取对应toast信息
                toastMsg = context.getString(R.string.bt_toast_4, deviceName);
            }
            Toast.makeText(context, toastMsg, Toast.LENGTH_SHORT).show();
        } 
        ....
 }

可以看到BlueoothOppReceiver其实是做了两件事儿

  1. 一是把文件传输这件事儿插入到传输队列中去传输
  2. 二是toast一条信息告诉用户,正在向谁传输文件,以及传输文件的数量

BlueoothOppReceier到这里结束了,紧接着去看mOppManager的startTransfer方法。方法定义在BluetoothOppManager

 /**
     * Fork a thread to insert share info to db.
     */
    public void startTransfer(BluetoothDevice device) {
        if (V) Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum);
        InsertShareInfoThread insertThread;
        synchronized (BluetoothOppManager.this) {
        //ALLOWED_INSERT_SHARE_THREAD_NUMBER的取值为3,mInsertShareThreadNum 是在每次创建文件传输线程时值会++,在线程结束后值会--文件传输线程数量如果大于3,则报错
            if (mInsertShareThreadNum > ALLOWED_INSERT_SHARE_THREAD_NUMBER) {
                Log.e(TAG, "Too many shares user triggered concurrently!");

                // Notice user
                Intent in = new Intent(mContext, BluetoothOppBtErrorActivity.class);
                in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                in.putExtra("title", mContext.getString(R.string.enabling_progress_title));
                in.putExtra("content", mContext.getString(R.string.ErrorTooManyRequests));
                mContext.startActivity(in);

                return;
            }
            //创建文件传输线程
            insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendingFile,
                    mUriOfSendingFile, mNameOfSendingFile, mMimeTypeOfSendingFiles, mUrisOfSendingFiles,
                    mIsHandoverInitiated);
            if (mMultipleFlag) {
            //如果是多文件传输,把要传输的文件数量存在mfileNumInBatch字段中
                mfileNumInBatch = mUrisOfSendingFiles.size();
            }
        }
         //开启文件传输线程
        insertThread.start();
    }

可以看到,在startTransfer方法中,首先会去判断文件传输线程是否超过上限(最大值为3),注意,这里所说的不是说文件传输数量,而是文件传输线程,由上述分析可知每当选择一个蓝牙设备进行分享时就会去创建一个文件传输线程。所以这里的上限是说在同一时刻最多可以向3个设备发送文件。 判断之后当然会有两个结果,如果超过了最大值则会报错,并且结束本次传输。如果没有超过文件传输线程所限定的最大值,则会继续创建文件分享线程去分享文件。所以,接下来就是要分析文件分享线程,线程代码依旧位于BluetoothOppManager类中。

/**线程用于将传输的文件插入到db中,因为当传输多个文件时(以传输100个文件为例)会是一个耗时操作,所以需要开启线程来处理。可以创建多个线程来实现对多个设备的文件传输。
     * Thread to insert share info to db. In multiple files (say 100 files)
     * share case, the inserting share info to db operation would be a time
     * consuming operation, so need a thread to handle it. This thread allows
     * multiple instances to support below case: User select multiple files to
     * share to one device (say device 1), and then right away share to second
     * device (device 2), we need insert all these share info to db.
     */
    private class InsertShareInfoThread extends Thread {
        private final BluetoothDevice mRemoteDevice;

        private final String mTypeOfSingleFile;

        private final String mUri;

        private final String mNameOfSingleFile;

        private final String mTypeOfMultipleFiles;

        private final ArrayList<Uri> mUris;

        private final boolean mIsMultiple;

        private final boolean mIsHandoverInitiated;

        public InsertShareInfoThread(BluetoothDevice device, boolean multiple,
                String typeOfSingleFile, String uri, String nameOfSingleFile,
                String typeOfMultipleFiles, ArrayList<Uri> uris,
                boolean handoverInitiated) {
            super("Insert ShareInfo Thread");
            //远程设备信息
            this.mRemoteDevice = device;
            //是否是要传输多个文件
            this.mIsMultiple = multiple;
            //传输的单个文件类型
            this.mTypeOfSingleFile = typeOfSingleFile;
            //传输单个文件的uri
            this.mUri = uri;
           //传输的单个文件的name
            this.mNameOfSingleFile = nameOfSingleFile;
            //传输多个文件的文件类型
            this.mTypeOfMultipleFiles = typeOfMultipleFiles;
            //传输多个文件的uris
            this.mUris = uris;
            //传输是否已经通过WiFi ,nfc等被初始化了
            this.mIsHandoverInitiated = handoverInitiated;

            synchronized (BluetoothOppManager.this) {
            //同步锁,对线程数量进行一个增量计算
                mInsertShareThreadNum++;
            }

            if (V) Log.v(TAG, "Thread id is: " + this.getId());
        }

        @Override
        public void run() {
    //设置线程优先级为后台Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            if (mRemoteDevice == null) {
                Log.e(TAG, "Target bt device is null!");
                return;
            }
            if (mIsMultiple) {
                //传输多个文件
                insertMultipleShare();
            } else {
                //传输单个文件
                insertSingleShare();
            }
            synchronized (BluetoothOppManager.this) {
            //在线程完成文件插入到db的操作后,对线程数量进行减量计算
                mInsertShareThreadNum--;
            }
        }

        /**
        插入多个文件到db,只能被OPP应用程序调用
         * Insert multiple sending sessions to db, only used by Opp application.
         */
        private void insertMultipleShare() {
            int count = mUris.size();
            Long ts = System.currentTimeMillis();
            for (int i = 0; i < count; i++) {
                Uri fileUri = mUris.get(i);

                BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(fileUri);
                ContentValues values = new ContentValues();
                values.put(BluetoothShare.URI, fileUri.toString());

                ContentResolver contentResolver = mContext.getContentResolver();
                fileUri = BluetoothOppUtility.originalUri(fileUri);
                String contentType = contentResolver.getType(fileUri);
                if (V) Log.v(TAG, "Got mimetype: " + contentType + "  Got uri: " + fileUri);
                if (TextUtils.isEmpty(contentType)) {
                    contentType = mTypeOfMultipleFiles;
                }
                values.put(BluetoothShare.MIMETYPE, contentType);
                values.put(BluetoothShare.DESTINATION, mRemoteDevice.getAddress());
                values.put(BluetoothShare.TIMESTAMP, ts);
                values.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
                if (mIsHandoverInitiated) {
                    values.put(BluetoothShare.USER_CONFIRMATION,
                            BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
                }
                final Uri contentUri = mContext.getContentResolver().insert(
                        BluetoothShare.CONTENT_URI, values);
                if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
                            + getDeviceName(mRemoteDevice));
            }
        }

         /**
         插入单个文件到db,只能被Opp应用程序调用
         * Insert single sending session to db, only used by Opp application.
         */
        private void insertSingleShare() {
            ContentValues values = new ContentValues();
            //问价uri
            values.put(BluetoothShare.URI, mUri);
            //文件名
            values.put(BluetoothShare.FILENAME_HINT, mNameOfSingleFile);
            //文件类型
            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);
            if (V) Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: "
                                + getDeviceName(mRemoteDevice));
        }
    }

第一,传入线程所需参数 第二,设置线程优先级为后台,这样可以减少对cpu的占用,当多个分享线程并发时减少对主线程的影响。 第三,记录文件分享线程数量值mInsertShareThreadNum 第四,将要传输的文件插入到db中,如果是多个文件就调用insertMultipleShare插入,如果是单个文件就调用insertSingleShare插入。 该线程也就这么点儿事儿,也许到现在你该奇怪了,文件传输到底在哪儿?怎么就把文件插入到db就结束了?? 难道你以为insert就只是简简单单的插入db中吗??那你就大错特错了。到现在为止,你将你要传输的文件交给了db,接下来就是ContentProvider的处理了。在介绍provider中的处理之前,先总结下从开始分享到交给provider的流程。

大致流程如下: 第一步,BluetoothOppLauncherActivity文件,选择蓝牙分享后会启动该activity(但是该activity主题为透明的,所以相当于瞒着用户启动了一个界面),在启动之后会进行判断是否开启蓝牙,如果没有开启就去开启蓝牙,如果蓝牙已经开启就打开蓝牙选择界面 第二步,DevicePickerActiviy文件:蓝牙选择界面。首先会负责加载蓝牙设备,在点击选择蓝牙设备后会先去判断是否可以发送文件(本机设备是否和远程蓝牙设备已完成配对,或者是远程蓝牙设备在接受文件时是否要授权)。如果可以就发送广播触发BluetoothOppReceiver,如果不可以就去营造条件 第三步,BluetoothOppReceive文件:一是通知系统要发送文件二是通知用户 第四步,BluetoothOppManager文件:启动线程将要发送的文件插入到db中。

在插入db时,uri为:

/**
     * The content:// URI for the data table in the provider
     */
    public static final Uri CONTENT_URI = Uri.parse("content://com.android.bluetooth.opp/btopp");

根据uri的域名com.android.bluetooth.opp找到所对应的provider为BluetoothOppProvider。进入到该文件中看insert方法

sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES);
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID);

匹配BluetoothShare.CONTENT_URI的id为SHARES

BluetoothOppProvider中的insert方法起到两个作用

  1. 将要发送的文件插入到db中
  2. 启动BluetoothOppService去传输文件

代码分析:

  • 代码236-242行,因为在这里还需要对key所对应的value进行判断或者赋值,所以先将values中的一部分key对应的值复制到filteredValues
    1. BluetoothShare.URI:所发送或者接受的文件的uri
    2. BluetoothShare.FILENAME_HINT:文件名
    3. BluetoothShare.MIMETYPE:文件类型
    4. BluetoothShare.DESTINATION:目标设备
  • 第246行,BluetoothShare.DIRECTION:获取到文件传输方向。这是因为文件传入和传出的处理在一个类中,只是标志不同而已。如果是传出文件,DIRECTION对应的value为null,此时系统会将传输方向赋值为BluetoothShare.DIRECTION_OUTBOUND即传出文件(代码250行-251行)
  • 第247行,BluetoothShare.USER_CONFIRMATION:是否需要用户确认。即在传出文件时是否需要用户确认文件传出?有6个取值
    • BluetoothShare.USER_CONFIRMATION_PENDING:This transfer is waiting for user confirmation,等待用户确认传输
    • BluetoothShare.USER_CONFIRMATION_CONFIRMED:This transfer is confirmed by user用户已经确认传输
    • BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED: This transfer is auto-confirmed per previous user confirmation通过先前的用户信息自动确认
    • BluetoothShare.USER_CONFIRMATION_DENIED:This transfer is denied by user文件传输被拒绝
    • BluetoothShare.USER_CONFIRMATION_TIMEOUT:This transfer is timeout before user action,文件传输超时
    • BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED:This transfer was initiated by a connection handover(for example WIFI, NFC) and has been auto-confirmed ,传输是由连接的切换引起的(比如WiFi或者nfc),并且已经自动确认

    如果是传出文件,则用户无需手动确认,也正如平常所见,在使用蓝牙分享文件时不会去询问用户是否分享。在253-254行代码对con进行赋值

  • 第256-257行代码,如果是传入文件,则需要用户确认,即在蓝牙传入文件时会需要用户选择是否接受文件,如果用户经过一定时间未处理,则会出现文件传输超时的问题,对于文件传输超时的时间的定义的字段为BluetoothOppObexSession.SESSION_TIMEOUT=5000,即如果文件接受方在5秒之内没有处理文件发送请求,那么文件就会传输超时停止传输。 注,题外话,对于文件超时的判断机理如下,在开始发送一个文件时延时SESSION_TIMEOUT向一个handler发送message,在对方开始接受文件后就移除该messge。一般源码上的一些判断超时的操作的机制大抵如此:即在事件开始之时延时启动线程或者是handler之类,所延时的时间即为判断超时的时间,待事件开始处理后就移除刚才的所要延时触发的动作。这种设计方式运用到应用开发中也是极好的。所以在研究源码的过程中不仅要明白这段代码是什么意思,更要看这段代码的实现原理有什么值得学习的地方
  • 271-272和279-280行代码均是启动service,一直没明白为什么要这样做。明明启动了,为什么还要再启动一遍?暂且留个悬念吧
  • 第274行代码:将要分享的文件插入插入数据库中
  • 第281行代码:用于触发该provider所注册的observer。只有显示的调用notifyChange方法,才会回调用户自定义的观察者的observe的onChange

ok,到现在为止,也差不多了,本以为传出文件代码会很好分析,没想到断断续续分析了这么多天,而且篇幅这么长,依旧没有完…….我也是很无语,不知不觉就罗嗦了一大堆,紧接着就该BluetoothOppService来分析了,也该进入下一篇了。太长的篇幅知道你们也没耐心看哈哈哈

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的Python

Scrapy分布式爬虫打造搜索引擎-(五)爬虫与反爬虫的战争Python分布式爬虫打造搜索引擎

Python分布式爬虫打造搜索引擎 基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站 五、爬虫与反爬虫 1. 基...

4804
来自专栏Phoenix的Android之旅

其实热修复就这么简单

上几篇内容介绍了Java的ClassLoader和相关的知识点,总的来说 · Java加载class逻辑是双亲委托模式 · 对于不在class path中的cl...

781
来自专栏菩提树下的杨过

thrift中的超时(timeout)坑

最近在项目中采用thrift作为后台服务rpc框架,总体用下来性能还不错,跨语言特性使用起来也还行,但是也遇到了一些坑,其中之一就是超时问题(timeout),...

6279
来自专栏FreeBuf

VirtualApp技术黑产利用研究报告

一、 前言 VirtualApp(以下称VA)是一个App虚拟化引擎(简称VA)。VirtualApp创建了一个虚拟空间,你可以在虚拟空间内任意的安装、启动和卸...

3488
来自专栏文大师的新世界

9. redux如何精简代码

通过之前的代码不难看出redux系统里的ActionType、Action、Reducer都有一定的共性,小项目无所谓,这样写更清晰,但是一旦组件以及业务增多,...

1695
来自专栏逸鹏说道

bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序

bootstrap + requireJS+ director+ knockout + web API = 一个时髦的单页程序 也许单页程序(Single Pa...

3365
来自专栏君赏技术博客

怎么让继承的类直接使用XIB的布局试图

最近做的一个小工具,一键替换key,就是为了解放双手,不然每次运行测试和正式的版本都要手动的替换key。

1572
来自专栏顶级程序员

MVC, MVP, MVVM比较以及区别

作者:JustRun 原文:cnblogs.com/JustRun1983/p/3679827.html MVC, MVP和MVVM都是用来解决界面呈现和逻辑...

50210
来自专栏程序员互动联盟

【专业技术第十讲】嵌入式系统的中断流程剖析

存在问题: 搞嵌入式特别是底层,常常提到中断,中断时干什么的呢? 解决方案: 做嵌入式肯定要了解中断。本文根据实例详细介绍中断过程,包括软件和硬件方面。 示例:...

4156
来自专栏24K纯开源

用Qt写软件系列三:一个简单的系统工具(上)

导言       继上篇《用Qt写软件系列二:QIECookieViewer》之后,有一段时间没有更新博客了。这次要写的是一个简单的系统工具,需求来自一个内部项...

4138

扫码关注云+社区

领取腾讯云代金券