Android 蓝牙 OPP文件传输流程分析

1,668 阅读14分钟

场景:大家应该都使用过Android手机,通过蓝牙发送文件到另一个手机的场景。简单说Android分享文件使用的就是OPP协议。

所以接下来,就开始分析下主动分享文件流程。

主要分为两部分,第一部分是从Framework层的Bluetooth分析调用流程,第二部分重点表述下文件传输流程。

Bluetooth流程

从实际的操作来看,分享它经历的过程主要为,首先选择蓝牙分享,第二如果蓝牙是打开的状态,会跳转到蓝牙界面,选择一个设备进行分享。

所以本文章,开始的流程就是从点击了“bluetooth”选项之后发生的流程。

一、分享入口

但我们点击蓝牙分享后,会进入到Bluetooth模块的Bluetooth\app\src\main\java\com\android\bluetooth\opp\BluetoothOppLauncherActivity.java

它是一个没有UI的Activity,相当一个中转处理站,一言不合就看下源码:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Intent intent = getIntent();
    String action = intent.getAction();
    .....
    // 1、分别表示的是单文件的分享和多文件的分享
    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;
        }
        
        // 通过isReadyForFileSharing标识,来标志之前分享正在处理中
        if (!BluetoothOppManager.isReadyForFileSharing) {
            Log.i(TAG, " File share already in process, retrun with out any action ");
            finish();
            return;
        }
        /*
         * SECURITY_EXCEPTION Google Photo grant-uri-permission
         */
        BTOppUtils.grantPermissionToUri(getApplicationContext(),
                intent.getClipData());

        /*
         * Other application is trying to share a file via Bluetooth,
         * probably Pictures, videos, or vCards. The Intent should contain
         * an EXTRA_STREAM with the data to attach.
         */
        if (action.equals(Intent.ACTION_SEND)) {
            // TODO: handle type == null case
            final String type = intent.getType();
            final Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
            CharSequence extraText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);

            if (stream != null && type != null) {
                if (V) {
                    Log.v(TAG,
                            "Get ACTION_SEND intent: Uri = " + stream + "; mimetype = " + type);
                }
                // Save type/stream, will be used when adding transfer
                // session to DB.
                Thread t = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        sendFileInfo(type, stream.toString(), false /* isHandover */, true /*
                         fromExternal */);
                    }
                });
                t.start();
                return;
            } else if (extraText != null && type != null) {
                if (V) {
                    Log.v(TAG,
                            "Get ACTION_SEND intent with Extra_text = " + extraText.toString()
                                    + "; mimetype = " + type);
                }
                final Uri fileUri = creatFileForSharedContent(
                        this.createCredentialProtectedStorageContext(), extraText);
                if (fileUri != null) {
                    Thread t = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            sendFileInfo(type, fileUri.toString(), false /* isHandover */,
                                    false /* fromExternal */);
                        }
                    });
                    t.start();
                    return;
                } else {
                    Log.w(TAG, "Error trying to do set text...File not created!");
                    finish();
                    return;
                }
            } else {
                Log.e(TAG, "type is null; or sending file URI is null");
                finish();
                return;
            }
        } else if (action.equals(Intent.ACTION_SEND_MULTIPLE)) {
                // 多文件分享逻辑省略
                .......
                return;
            } else {
                Log.e(TAG, "type is null; or sending files URIs are null");
                finish();
                return;
            }
        }
    } else if (action.equals(Constants.ACTION_OPEN)) {
        Uri uri = getIntent().getData();
        if (V) {
            Log.v(TAG, "Get ACTION_OPEN intent: Uri = " + uri);
        }

        Intent intent1 = new Intent();
        intent1.setAction(action);
        intent1.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
        intent1.setDataAndNormalize(uri);
        this.sendBroadcast(intent1);
        finish();
    } else {
        Log.w(TAG, "Unsupported action: " + action);
        finish();
    }
}

上面的代码,省略了一些不重要的逻辑。下面看下分享的逻辑。

  • 1、第一步是判断是不是分享,它包括单文件和多文件

因为流程都差不多,所以下面只已单文件分享为例。

  • 2、isBluetoothAllowed() 检查是否允许蓝牙传输,比如在飞行模式下是否允许等,这是第一步判断
private boolean isBluetoothAllowed() {
    final ContentResolver resolver = this.getContentResolver();

    // Check if airplane mode is on
    // 1、如果飞行模式关闭,直接返回TRUE
    final boolean isAirplaneModeOn =
            Settings.System.getInt(resolver, Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
    if (!isAirplaneModeOn) {
        return true;
    }

    // Check if airplane mode matters
    // 2、如果飞行模式打开,但飞行模式不重要。返回TRUE
    final String airplaneModeRadios =
            Settings.System.getString(resolver, Settings.Global.AIRPLANE_MODE_RADIOS);
    final boolean isAirplaneSensitive =
            airplaneModeRadios == null || airplaneModeRadios.contains(
                    Settings.Global.RADIO_BLUETOOTH);
    if (!isAirplaneSensitive) {
        return true;
    }

    // Check if Bluetooth may be enabled in airplane mode
    // 3、如果飞行模式打开,但允许蓝牙操作。返回TRUE
    final String airplaneModeToggleableRadios = Settings.System.getString(resolver,
            Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS);
    final boolean isAirplaneToggleable =
            airplaneModeToggleableRadios != null && airplaneModeToggleableRadios.contains(
                    Settings.Global.RADIO_BLUETOOTH);
    if (isAirplaneToggleable) {
        return true;
    }

    // If we get here we're not allowed to use Bluetooth right now
    return false;
}
  • 3、单个文件下面分为了两类:
    • ①、stream != null && type != null : 文件类型
    • ②、extraText != null && type != null : 文本类型

从这可以看到,蓝牙传输可以是文件类型或者文本类型,对于传输来说,它都只是一个二进制。

  • 4、至此,我们可以看到,所有的前期判断准备都已做好了。最后就会走到真实的处理逻辑 sendFileInfo(type, stream.toString(), false /* isHandover /, true / fromExternal */);

二、打开蓝牙

前面流程都是一些准备工作,sendFileInfo才算是要准备要发送了。

但这里也不能是真实的发送。因为有一个问题:蓝牙有没有打开?,如果蓝牙都没有打开,那是不可能进行下一步处理的。

private void sendFileInfo(String mimeType, String uriString, boolean isHandover,
        boolean fromExternal) {
    BluetoothOppManager manager = BluetoothOppManager.getInstance(getApplicationContext());
    try {
        // 首先改变标识,这样其它的分享暂时就进不来了
        BluetoothOppManager.isReadyForFileSharing = false;
        // 再将当前的任务保存到文件中,这样当蓝牙服务重新启动的时候,可以恢复
        manager.saveSendingFileInfo(mimeType, uriString, isHandover, fromExternal);
        // 核心逻辑:如果尚未打开蓝牙,则打开蓝牙,如果蓝牙打开,则启动设备选择器
        launchDevicePicker();
        finish();
    } catch (IllegalArgumentException exception) {
        Log.e(TAG, "sendFileInfo :" + exception.getMessage());
        finish();
        BluetoothOppManager.isReadyForFileSharing = true;
    }
}

这里只需要看下launchDevicePicker里面的逻辑,它主要负责蓝牙开关处理。如果尚未打开蓝牙,则打开蓝牙,如果蓝牙打开,则启动设备选择器。

private 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 {
        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);
    }
    BluetoothOppManager.isReadyForFileSharing = true;
}

到了这里,又分成了两个流程:

  • 蓝牙未打开时候,跳转到BluetoothOppBtEnableActivity界面打开蓝牙
  • 蓝牙打开了,跳转到蓝牙设备选择界面去处理发送逻辑。

然后再将isReadyForFileSharing变量制成TRUE,这样在蓝牙进程的流程已经走完了。

(一)、BluetoothOppBtEnableActivity界面打开蓝牙

image.png

一图胜千言,这个就是BluetoothOppBtEnableActivity的界面,提供了两个按钮,这里只需要关系“TURN ON”键。

看下点击事件对应逻辑:

@Override
public void onClick(DialogInterface dialog, int which) {
    switch (which) {
        case DialogInterface.BUTTON_POSITIVE:
        // 打开蓝牙开关
            mOppManager.enableBluetooth(); // this is an asyn call
            mOppManager.mSendingFlag = true;

            Toast.makeText(this, getString(R.string.enabling_progress_content),
                    Toast.LENGTH_SHORT).show();
        // 再进入BluetoothOppBtEnablingActivity等待蓝牙打开
            Intent in = new Intent(this, BluetoothOppBtEnablingActivity.class);
            in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            this.startActivity(in);

            finish();
            break;

        case DialogInterface.BUTTON_NEGATIVE:
            finish();
            break;
    }
}

分成了两部分:

  • mOppManager.enableBluetooth(); 打开蓝牙
            /**
             * Enable Bluetooth hardware.
             */
            public void enableBluetooth() {
                if (mAdapter != null) {
                    mAdapter.enable();
                }
            }
  • 跳转到BluetoothOppBtEnablingActivity等待蓝牙打开,再做处理
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // If BT is already enabled jus return.
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    if (adapter.isEnabled()) {
        finish();
        return;
    }

    IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
    registerReceiver(mBluetoothReceiver, filter);
    mRegistered = true;

    // Set up the "dialog"
    final AlertController.AlertParams p = mAlertParams;
    p.mTitle = getString(R.string.enabling_progress_title);
    p.mView = createView();
    setupAlert();

    // Add timeout for enabling progress
    mTimeoutHandler.sendMessageDelayed(mTimeoutHandler.obtainMessage(BT_ENABLING_TIMEOUT),
            BT_ENABLING_TIMEOUT_VALUE);
}

private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (V) {
            Log.v(TAG, "Received intent: " + action);
        }
        if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
            switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
                case BluetoothAdapter.STATE_ON:
                    mTimeoutHandler.removeMessages(BT_ENABLING_TIMEOUT);
                    finish();
                    break;
                default:
                    break;
            }
        }
    }
};

这里只分析下主要流程:

  • 如果蓝牙打开了,直接finish
  • 注册一个监听蓝牙打开的广播,如果蓝牙打开了,finish界面
  • 显示progress UI
  • 发送一个20S的超时事件,如果超时也取消

看下界面:

image.png

猜想下:上面进入的时候,如果蓝牙是打开的,则会直接进入到BluetoothDevicePicker.ACTION_LAUNCH蓝牙设备选择界面。那么没打开,进入到BluetoothOppBtEnableActivity界面打开蓝牙后,是不是也是进入到BluetoothDevicePicker.ACTION_LAUNCH蓝牙设备选择界面去传输数据了,如果是跳入的入口在哪了?

BluetoothOppService中,会注册一个状态

private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (D) Log.d(TAG, "action : " + action);
        if (action == null) return;
        if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
            switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
                case BluetoothAdapter.STATE_ON:
                    if (V) {
                        Log.v(TAG, "Bluetooth state changed: STATE_ON");
                    }
                    startListener();
                    // If this is within a sending process, continue the handle
                    // logic to display device picker dialog.
                    synchronized (this) {
                        // 判断是否有发送的任务,没有则不启动
                        if (BluetoothOppManager.getInstance(context).mSendingFlag) {
                            // reset the flags
                            BluetoothOppManager.getInstance(context).mSendingFlag = false;

                            Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
                            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());

                            in1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            context.startActivity(in1);
                        }
                    }

                    break;
                case BluetoothAdapter.STATE_TURNING_OFF:
                    if (V) {
                        Log.v(TAG, "Bluetooth state changed: STATE_TURNING_OFF");
                    }
                    mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
                    break;
            }
        } else {
            BTOppUtils.checkAction(intent);
        }
    }
};

service启动的时候,会注册广播监听蓝牙状态,蓝牙打开的时候,也会进入到蓝牙选择界面。

三、文件传输

(一)、选择目标传输Device

蓝牙打开后,此刻肯定是需要确定一个设备来充当目标端,这样我们可以创建连接了。

BLuetooth进程通过隐式启动framework/base/core/java/android/bluetooth/BluetoothDevicePicker.java,BluetoothDevicePicker其实是Setting中的界面。

看下package/apps/Settings/AndroidManifest.xml Setting的清单文件

        <activity android:name=".bluetooth.DevicePickerActivity"
                android:label="@string/device_picker"
                android:configChanges="orientation|keyboardHidden|screenSize"
                android:clearTaskOnLaunch="true">
            <intent-filter>
                <action android:name="android.bluetooth.devicepicker.action.LAUNCH" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

其实DevicePickerActivity只是一个载体,真实的处理是放在DevicePickerFragment中。DevicePickerFragment是显示扫描到的蓝牙。

image.png

    @Override
    void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
        disableScanning();
        LocalBluetoothPreferences.persistSelectedDeviceInPicker(
                getActivity(), mSelectedDevice.getAddress());
        if ((btPreference.getCachedDevice().getBondState() ==
                BluetoothDevice.BOND_BONDED) || !mNeedAuth) {
            sendDevicePickedIntent(mSelectedDevice);
            finish();
        } else {
            super.onDevicePreferenceClick(btPreference);
        }
    }
    
    
    private void sendDevicePickedIntent(BluetoothDevice device) {
        android.util.Log.d("Devicepicker", "sendDevicePickedIntent");
        Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
        if (mLaunchPackage != null && mLaunchClass != null) {
            intent.setClassName(mLaunchPackage, mLaunchClass);
        }
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        getActivity().sendBroadcast(intent);
    }

当选中一个设备后,会执行onDevicePreferenceClick方法,因为mNeedAuth=FALSE,所以会走到sendDevicePickedIntent方法体。

sendDevicePickedIntent可以看到是发送了BluetoothDevicePicker.ACTION_DEVICE_SELECTED广播出去。看来Setting中也不是一个处理者,而是确定远端的一个中间体。

(二)、将Session插入的DB中

再回到Bluetooth应用中看下BluetoothOppReceiver接受端

public class BluetoothOppReceiver extends BroadcastReceiver {
    private static final String TAG = "BluetoothOppReceiver";
    private static final boolean D = Constants.DEBUG;
    private static final boolean V = Constants.VERBOSE;

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(D) Log.d(TAG, "Action :" + action);
        if (action == null) return;
        // 进入到这里处理
        if (action.equals(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) {
            BluetoothOppManager mOppManager = BluetoothOppManager.getInstance(context);
            BluetoothOppManager.isReadyForFileSharing = false;
            BluetoothDevice remoteDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            if (D) {
                Log.d(TAG, "Received BT device selected intent, bt device: " + remoteDevice);
            }

            if (remoteDevice == null) {
                mOppManager.cleanUpSendingFileInfo();
                BluetoothOppManager.isReadyForFileSharing = true;
                return;
            }
            // Insert transfer session record to database
            mOppManager.startTransfer(remoteDevice);

            // Display toast message
            String deviceName = mOppManager.getDeviceName(remoteDevice);
            String toastMsg;
            int batchSize = mOppManager.getBatchSize();
            if (mOppManager.mMultipleFlag) {
                toastMsg = context.getString(R.string.bt_toast_5, Integer.toString(batchSize),
                        deviceName);
            } else {
                toastMsg = context.getString(R.string.bt_toast_4, deviceName);
            }
            BluetoothOppManager.isReadyForFileSharing = true;
            Toast.makeText(context, toastMsg, Toast.LENGTH_SHORT).show();
        }

从上面流程可以看出,主要就调用mOppManager.startTransfer(remoteDevice),传入Setting中传入的remoteDevice;并Toast相关Tip。

再看下startTransfer方法,它主要是将传输会话记录插入数据库,之后它的任务就完成了。

public void startTransfer方法,(BluetoothDevice device) {
    if (V) {
        Log.v(TAG, "Active InsertShareThread number is : " + mInsertShareThreadNum);
    }
    InsertShareInfoThread insertThread;
    synchronized (BluetoothOppManager.this) {
        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, mMimeTypeOfSendingFiles, mUrisOfSendingFiles,
                mIsHandoverInitiated);
        if (mMultipleFlag) {
            mFileNumInBatch = mUrisOfSendingFiles.size();
        }
    }

    insertThread.start();
}

因为操作DB是一个耗时的工作,所以是要开启一个线程去处理。然后是要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);
    if (V) {
        Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: " + getDeviceName(
                mRemoteDevice));
    }
}

这里组装成ContentValues对象,然后再通过ContentProvider来插入到数据库中。到此还处在消息的生产端,那么消息什么时候会被消费了?

ContentProvider的实现类为BluetoothOppProvider,所以看下BluetoothOppProvider中的insert方法

@Override
public Uri insert(Uri uri, ContentValues values) {
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();

    if (sURIMatcher.match(uri) != SHARES) {
        throw new IllegalArgumentException("insert: Unknown/Invalid URI " + uri);
    }

    ContentValues filteredValues = new ContentValues();

    copyString(BluetoothShare.URI, values, filteredValues);
    copyString(BluetoothShare.FILENAME_HINT, values, filteredValues);
    copyString(BluetoothShare.MIMETYPE, values, filteredValues);
    copyString(BluetoothShare.DESTINATION, values, filteredValues);

    copyInteger(BluetoothShare.VISIBILITY, values, filteredValues);
    copyLong(BluetoothShare.TOTAL_BYTES, values, filteredValues);
    if (values.getAsInteger(BluetoothShare.VISIBILITY) == null) {
        filteredValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_VISIBLE);
    }
    Integer dir = values.getAsInteger(BluetoothShare.DIRECTION);
    Integer con = values.getAsInteger(BluetoothShare.USER_CONFIRMATION);

    if (dir == null) {
        dir = BluetoothShare.DIRECTION_OUTBOUND;
    }
    if (dir == BluetoothShare.DIRECTION_OUTBOUND && con == null) {
        con = BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED;
    }
    if (dir == BluetoothShare.DIRECTION_INBOUND && con == null) {
        con = BluetoothShare.USER_CONFIRMATION_PENDING;
    }
    filteredValues.put(BluetoothShare.USER_CONFIRMATION, con);
    filteredValues.put(BluetoothShare.DIRECTION, dir);

    filteredValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_PENDING);
    filteredValues.put(Constants.MEDIA_SCANNED, 0);

    Long ts = values.getAsLong(BluetoothShare.TIMESTAMP);
    if (ts == null) {
        ts = System.currentTimeMillis();
    }
    filteredValues.put(BluetoothShare.TIMESTAMP, ts);

    Context context = getContext();

    long rowID = db.insert(DB_TABLE, null, filteredValues);

    if (rowID == -1) {
        Log.w(TAG, "couldn't insert " + uri + "into btopp database");
        return null;
    }

    context.getContentResolver().notifyChange(uri, null);

    return Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID);
}
  • 1、通过SQLiteDatabase插入的DB_TABLE表中
  • 2、然后再通知notifyChange监听了BluetoothShare.CONTENT_URI数据有变化

(三)、Session的消费

在BluetoothOppService中被创建的时候,会注册BluetoothShareContentObserver,以监听BluetoothShare.CONTENT_URI,当有变化的时候,在mObserver处理

mObserver = new BluetoothShareContentObserver();
getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI,
        true, mObserver);
 
private class BluetoothShareContentObserver extends ContentObserver {

    BluetoothShareContentObserver() {
        super(new Handler());
    }

    @Override
    public void onChange(boolean selfChange) {
        if (V) {
            Log.v(TAG, "ContentObserver received notification");
        }
        updateFromProvider();
    }
}
 
private void updateFromProvider() {
    synchronized (BluetoothOppService.this) {
        mPendingUpdate = true;
        if (mUpdateThread == null) {
            mUpdateThread = new UpdateThread();
            mUpdateThread.start();
            mUpdateThreadRunning = true;
        }
    }
}

监听数据库有变化之后,会在调用UpdateThread开启个线程来处理新插入的数据。在看下UpdateThread的处理逻辑。

UpdateThread中的run方法,核心的就是insertShare

private void insertShare(Cursor cursor, int arrayPos) {
    String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
    Uri uri;
    if (uriString != null) {
        uri = Uri.parse(uriString);
        Log.d(TAG, "insertShare parsed URI: " + uri);
    } else {
        uri = null;
        Log.e(TAG, "insertShare found null URI at cursor!");
    }
    BluetoothOppShareInfo info = new BluetoothOppShareInfo(
            cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)), uri,
            cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
            cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
            cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
            cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
            cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
            cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
            cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
            cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
            cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
            cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
            cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
            cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED))
                    != Constants.MEDIA_SCANNED_NOT_SCANNED);


    mShares.add(arrayPos, info);

    /* Mark the info as failed if it's in invalid status */
    if (info.isObsolete()) {
        Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
    }
    /*
     * Add info into a batch. The logic is
     * 1) Only add valid and readyToStart info
     * 2) If there is no batch, create a batch and insert this transfer into batch,
     * then run the batch
     * 3) If there is existing batch and timestamp match, insert transfer into batch
     * 4) If there is existing batch and timestamp does not match, create a new batch and
     * put in queue
     */

    if (info.isReadyToStart()) {
        ....
        
        if (mBatches.size() == 0) {
            BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
            newBatch.mId = mBatchId;
            mBatchId++;
            mBatches.add(newBatch);
            // 发送文件
            if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
               
                mTransfer = new BluetoothOppTransfer(this, newBatch);
            } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) { // 接收文件
                
                mServerTransfer = new BluetoothOppTransfer(this, newBatch, mServerSession);
            }

            if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
                if (V) {
                    Log.v(TAG, "Service start transfer new Batch " + newBatch.mId + " for info "
                            + info.mId);
                }
                // 启动发送逻辑
                mTransfer.start();
            } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
                    && mServerTransfer != null) {
                if (V) {
                    Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
                            + " for info " + info.mId);
                }
                mServerTransfer.start();
            }

        } else {
            int i = findBatchWithTimeStamp(info.mTimestamp);
            if (i != -1) {
                if (V) {
                    Log.v(TAG, "Service add info " + info.mId + " to existing batch " + mBatches
                            .get(i).mId);
                }
                mBatches.get(i).addShare(info);
            } else {
                // There is ongoing batch
                BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
                newBatch.mId = mBatchId;
                mBatchId++;
                mBatches.add(newBatch);
                if (V) {
                    Log.v(TAG,
                            "Service add new Batch " + newBatch.mId + " for info " + info.mId);
                }
            }
        }
    }
}

调用start方法,创建BlueSocket连接,用于传输文件。

public void start() {
    /* check Bluetooth enable status */
    /*
     * normally it's impossible to reach here if BT is disabled. Just check
     * for safety
     */
    if (!mAdapter.isEnabled()) {
        Log.e(TAG, "Can't start transfer when Bluetooth is disabled for " + mBatch.mId);
        markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR);
        mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
        return;
    }

    if (mHandlerThread == null) {
        if (V) {
            Log.v(TAG, "Create handler thread for batch " + mBatch.mId);
        }
        mHandlerThread =
                new HandlerThread("BtOpp Transfer Handler", Process.THREAD_PRIORITY_BACKGROUND);
        mHandlerThread.start();
        mSessionHandler = new EventHandler(mHandlerThread.getLooper());

        if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
            /* for outbound transfer, we do connect first */
            startConnectSession();
        } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
            /*
             * for inbound transfer, it's already connected, so we start
             * OBEX session directly
             */
            startObexSession();
        }
    }
    // 监听连接的状态变化
    registerConnectionreceiver();
}

因为第一次mHandlerThread = null,以及mBatch.mDirection = BluetoothShare.DIRECTION_OUTBOUND,所以会走到startConnectSession中,即创建一个连接

然后registerConnectionreceiver来监听连接状态,是不是连接成功了,连接成功了就可以传输操作了。

再看下startConnectSession方法

private void startConnectSession() {
    mDevice = mBatch.mDestination;
    // 主要的方法是查询刚传入的设备
    if (!mBatch.mDestination.sdpSearch(BluetoothUuid.ObexObjectPush)) {
        if (D) {
            Log.d(TAG, "SDP failed, start rfcomm connect directly");
        }
        /* update bd address as sdp could not be started */
        mDevice = null;
        /* SDP failed, start rfcomm connect directly */
        mConnectThread = new SocketConnectThread(mBatch.mDestination, false, false, -1);
        mConnectThread.start();
    }
}

sdpSearch的逻辑,从字段理解就是通过sdp协议查询设备。它的流程就是通过Binder进入到AdapterService服务中开启查询,但还是委托给SdpManager类做处理。

下面列出的是AdapterService的逻辑,可见返回的是true,所以上列逻辑不会继续执行了。

boolean sdpSearch(BluetoothDevice device, ParcelUuid uuid) {
    enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
    if (mSdpManager != null) {
        mSdpManager.sdpSearch(device, uuid);
        return true;
    } else {
        return false;
    }
}

再看下SdpManager中的逻辑 mSdpManager.sdpSearch(device, uuid);



/* Caller must hold the mTrackerLock */
// 开始寻找
private void startSearch() {

    SdpSearchInstance inst = sSdpSearchTracker.getNext();

    if ((inst != null) && (!sSearchInProgress)) {
        if (D) {
            Log.d(TAG, "Starting search for UUID: " + inst.getUuid());
        }
        sSearchInProgress = true;

        inst.startSearch(); // Trigger timeout message
        // 进入到JNI方法中
        sdpSearchNative(Utils.getBytesFromAddress(inst.getDevice().getAddress()),
                Utils.uuidToByteArray(inst.getUuid()));
    } else { // Else queue is empty.
        if (D) {
            Log.d(TAG, "startSearch(): nextInst = " + inst + " mSearchInProgress = "
                    + sSearchInProgress + " - search busy or queue empty.");
        }
    }
}

// 如果有查询到,则C层会调用该java方法,已通知有找到
void sdpOppOpsRecordFoundCallback(int status, byte[] address, byte[] uuid, int l2capPsm,
        int rfcommCannelNumber, int profileVersion, String serviceName, byte[] formatsList,
        boolean moreResults) {

    synchronized (TRACKER_LOCK) {
        SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
        SdpOppOpsRecord sdpRecord = null;

        if (inst == null) {
            Log.e(TAG, "sdpOppOpsRecordFoundCallback: Search instance is NULL");
            return;
        }
        inst.setStatus(status);
        if (status == AbstractionLayer.BT_STATUS_SUCCESS) {
            sdpRecord = new SdpOppOpsRecord(serviceName, rfcommCannelNumber, l2capPsm,
                    profileVersion, formatsList);
        }
        if (D) {
            Log.d(TAG, "UUID: " + Arrays.toString(uuid));
        }
        if (D) {
            Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
        }
        // 这里面会发送广播给到我们的之前注册的地方
        sendSdpIntent(inst, sdpRecord, moreResults);
    }
}

SdpManager中会做两件事,第一件事是开启一个查询超时Message,如果超时则会发送FALSE的状态。如果找到了则会通过广播将数据发送出去。

所以到这里,再回到之前注册的广播这里。这里只截取一些核心的逻辑:

if (uuid.equals(BluetoothUuid.ObexObjectPush)) {
    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
    Log.d(TAG, " -> status: " + status);
    BluetoothDevice device =
            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    if (mDevice == null) {
        Log.w(TAG, "OPP SDP search, target device is null, ignoring result");
        return;
    }
    if (!device.getAddress().equalsIgnoreCase(mDevice.getAddress())) {
        Log.w(TAG, " OPP SDP search for wrong device, ignoring!!");
        return;
    }
    SdpOppOpsRecord record =
            intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
    if (record == null) {
        Log.w(TAG, " Invalid SDP , ignoring !!");
        markConnectionFailed(null);
        return;
    }
    // 启动一个线程去连接
    mConnectThread =
            new SocketConnectThread(mDevice, false, true, record.getL2capPsm());
    mConnectThread.start();
    mDevice = null;
}

我们可以看到将SdpManager中获取的数据传入,并开启一个线程来连接。所以再看下SocketConnectThread再做了什么,只截取主要代码

/* Use BluetoothSocket to connect */
// SocketConnectThread中的run方法中
try {
    if (mIsInterrupted) {
        Log.e(TAG, "btSocket connect interrupted ");
        markConnectionFailed(mBtSocket);
        return;
    } else {
    // 1、创建一个socket
        mBtSocket = mDevice.createInsecureL2capSocket(mL2cChannel);
    }
} catch (IOException e1) {
    Log.e(TAG, "L2cap socket create error", e1);
    connectRfcommSocket();
    return;
}
try {
// 2、开始连接
    mBtSocket.connect();
    if (D) {
        Log.v(TAG, "L2cap socket connection attempt took " + (System.currentTimeMillis()
                - mTimestamp) + " ms");
    }
    BluetoothObexTransport transport;
    transport = new BluetoothObexTransport(mBtSocket);
    BluetoothOppPreference.getInstance(mContext).setName(mDevice, mDevice.getName());
    if (V) {
        Log.v(TAG, "Send transport message " + transport.toString());
    }
    // 3、通过Handler发送一个消息,来开始发送
    mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget();
} catch (IOException e) {
    Log.e(TAG, "L2cap socket connect exception", e);
    try {
        mBtSocket.close();
    } catch (IOException e3) {
        Log.e(TAG, "Bluetooth socket close error ", e3);
    }
    connectRfcommSocket();
    return;
}

  • 1、创建一个socket, mDevice.createInsecureL2capSocket(mL2cChannel);
  • 2、开始连接 mBtSocket.connect();
  • 3、通过Handler发送一个消息,来开始发送

连接的Socket分为两种,一种是Rfcomm,一种是L2CAP。这里使用的是L2CAP,如果SdpManager不存在,则是Rfcomm方式。

(四)、发送数据

接着上面流程,BluetoothOppTransfer中通过startObexSession来处理创建的socket。

private void startObexSession() {

    mBatch.mStatus = Constants.BATCH_STATUS_RUNNING;
 
    ......

    if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
        if (V) {
            Log.v(TAG, "Create Client session with transport " + mTransport.toString());
        }
        mSession = new BluetoothOppObexClientSession(mContext, mTransport);
    } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
        ......
    }

    mSession.start(mSessionHandler, mBatch.getNumShares());
    processCurrentShare();
}

因为是作为发送端,所以会走到BluetoothOppObexClientSession中的start方法

@Override
public void start(Handler handler, int numShares) {
    if (D) {
        Log.d(TAG, "Start!");
    }
    mCallback = handler;
    mThread = new ClientThread(mContext, mTransport, numShares);
    mThread.start();
}
   
   
// BluetoothOppObexClientSession中的run方法
@Override
public void run() {
    .....
    
    if (!mInterrupted) {
    // 发起obex连接
        connect(mNumShares);
    }

    mNumFilesAttemptedToSend = 0;
    while (!mInterrupted) {
        if (!mWaitingForShare) {
        // 开始发送
            doSend();
        } else {
            try {
                if (D) {
                    Log.d(TAG, "Client thread waiting for next share, sleep for "
                            + SLEEP_TIME);
                }
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {

            }
        }
    }
    disconnect();

    if (mWakeLock.isHeld()) {
        if (V) {
            Log.v(TAG, "release partial WakeLock");
        }
        mWakeLock.release();
    }

    if (mNumFilesAttemptedToSend > 0) {
        // Log outgoing OPP transfer if more than one file is accepted by remote
        MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.OPP);
    }
    Message msg = Message.obtain(mCallback);
    msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
    msg.obj = mInfo;
    msg.sendToTarget();

}

再看下doSend中的逻辑,主要是检查文件等工作,在sendFile做真实的处理。

private int sendFile(BluetoothOppSendFileInfo fileInfo) {
    boolean error = false;
    int responseCode = -1;
    long position = 0;
    int status = BluetoothShare.STATUS_SUCCESS;
    Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
    ContentValues updateValues;
    HeaderSet request = new HeaderSet();
    ClientOperation putOperation = null;
    OutputStream outputStream = null;
    InputStream inputStream = null;
    try {
        synchronized (this) {
            mWaitingForRemote = true;
        }
        try {
            if (V) {
                Log.v(TAG, "Set header items for " + fileInfo.mFileName);
            }
            // 1、发送文件name、type等文件信息给远端
            request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
            request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);

            applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName);
            Constants.updateShareStatus(mContext1, mInfo.mId,
                    BluetoothShare.STATUS_RUNNING);

            request.setHeader(HeaderSet.LENGTH, fileInfo.mLength);

            if (V) {
                Log.v(TAG, "put headerset for " + fileInfo.mFileName);
            }
            putOperation = (ClientOperation) mCs.put(request);
        } catch (IllegalArgumentException e) {
            .......
            error = true;
        }
        synchronized (this) {
            mWaitingForRemote = false;
        }

        .......
        
        if (!error) {
            int readLength = 0;
            long percent = 0;
            long prevPercent = 0;
            boolean okToProceed = false;
            long timestamp = 0;
            long currentTime = 0;
            long prevTimestamp = SystemClock.elapsedRealtime();
            int outputBufferSize = putOperation.getMaxPacketSize();
            byte[] buffer = new byte[outputBufferSize];
            BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);

            if (!mInterrupted && (position != fileInfo.mLength)) {
                readLength = readFully(a, buffer, outputBufferSize);

                mCallback.sendMessageDelayed(mCallback.obtainMessage(
                        BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
                        BluetoothOppObexSession.SESSION_TIMEOUT);
                synchronized (this) {
                    mWaitingForRemote = true;
                }

                // first packet will block here
                // 2、发送第一个包给对方,
                outputStream.write(buffer, 0, readLength);

                position += readLength;

                if (position == fileInfo.mLength) {
                    // if file length is smaller than buffer size, only one packet
                    // so block point is here
                    outputStream.close();
                    outputStream = null;
                }

                /* check remote accept or reject */
                // 3、看对方是否接收,如果接收才能有接下来的数据发送
                responseCode = putOperation.getResponseCode();

                mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
                synchronized (this) {
                    mWaitingForRemote = false;
                }

                if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
                        || responseCode == ResponseCodes.OBEX_HTTP_OK) {
                    if (D) {
                        Log.v(TAG, "Remote accept");
                    }
                    okToProceed = true;
                    updateValues = new ContentValues();
                    updateValues.put(BluetoothShare.CURRENT_BYTES, position);
                    mContext1.getContentResolver()
                            .update(contentUri, updateValues, null, null);
                    mNumFilesAttemptedToSend++;
                } else {
                    Log.i(TAG, "Remote reject, Response code is " + responseCode);
                }
            }
            long beginTime = System.currentTimeMillis();
            /// 4、循环的发送数据,直到完成
            while (!mInterrupted && okToProceed && (position < fileInfo.mLength)) {
                if (V) {
                    timestamp = SystemClock.elapsedRealtime();
                }

                readLength = a.read(buffer, 0, outputBufferSize);
                outputStream.write(buffer, 0, readLength);

                /* check remote abort */
                responseCode = putOperation.getResponseCode();
                if (V) {
                    Log.v(TAG, "Response code is " + responseCode);
                }
                if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
                        && responseCode != ResponseCodes.OBEX_HTTP_OK) {
                    /* abort happens */
                    // 每次包发送完成都要检查传输是否是终止了
                    okToProceed = false;
                } else {
                    position += readLength;
                    currentTime = SystemClock.elapsedRealtime();
                    if (V) {
                        Log.v(TAG, "Sending file position = " + position
                                + " readLength " + readLength + " bytes took "
                                + (currentTime - timestamp) + " ms");
                    }
                    // Update the Progress Bar only if there is change in percentage
                    // or once per a period to notify NFC of this transfer is still alive
                    percent = position * 100 / fileInfo.mLength;
                    if (percent > prevPercent
                            || currentTime - prevTimestamp > Constants.NFC_ALIVE_CHECK_MS) {
                        updateValues = new ContentValues();
                        updateValues.put(BluetoothShare.CURRENT_BYTES, position);
                        mContext1.getContentResolver()
                                .update(contentUri, updateValues, null, null);
                        prevPercent = percent;
                        prevTimestamp = currentTime;
                    }
                }
            }

            if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
                    || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
                Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
                        + fileInfo.mLength);
                status = BluetoothShare.STATUS_FORBIDDEN;
            } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
                Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
                status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
            } else if (!mInterrupted && position == fileInfo.mLength) {
                Log.i(TAG,
                        "SendFile finished send out file " + fileInfo.mFileName + " length "
                                + fileInfo.mLength);
                BTOppUtils.throughputInKbps(fileInfo.mLength, beginTime);
            } else {
                error = true;
                status = BluetoothShare.STATUS_CANCELED;
                putOperation.abort();
                /* interrupted */
                Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
                        + " at " + position + " of " + fileInfo.mLength);
            }
        }
    } catch (IOException e) {
        handleSendException(e.toString());
    } catch (NullPointerException e) {
        handleSendException(e.toString());
    } catch (IndexOutOfBoundsException e) {
        handleSendException(e.toString());
    } finally {
        .........
    }
    BluetoothOppUtility.cancelNotification(mContext);
    return status;
}

  • 1、发送文件name、type等文件信息给远端,供对方显示
  • 2、发送第一个包给对方,
  • 3、看对方是否接收第一个包,如果接收才能有接下来的数据发送
  • 4、循环的发送数据,直到完成或被终止