转载请注明出处: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>  

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

在之后就是一些状态的处理了。到此通过蓝牙分享文件到流程基本上过了一遍,其中还有许多状态、进度等相关功还没能研究透彻,之后再继续研究。

欢迎扫一扫关注我的微信公众号,定期推送优质技术文章:

更多相关文章

  1. C语言函数的递归(上)
  2. Android(安卓)SDK中tools详解
  3. Android(安卓)利用命令生成keystore文件
  4. Android逆向之旅—解析编译之后的Resource.arsc文件格式
  5. 基于树莓派的 Android(安卓)Things 开发环境
  6. android关于百度地图显示网格问题
  7. android通过蓝牙连接打印机实现格式化打印(二)
  8. Android(安卓)Drawable Resource学习(二)、BitmapDrawable和Bitmap
  9. 在 ubuntu 下编译 android 找不到头文件问题解决

随机推荐

  1. Android实现内录
  2. Android(安卓)O: 触摸事件传递流程源码分
  3. Android的线程使用来更新UI----Thread、H
  4. Android源代码下载
  5. windows环境下进入到android 模拟器
  6. Android中改变Activity的不同icon:activit
  7. 收藏各种技术源码
  8. Activity学习日记(一)
  9. android:configChanges
  10. 探究Android之ClassLoader