Android Bluetooth OPP

本篇博客,分析Android Bluetooth的Object Push Profile,分别是架构、代码流程,日志打印,HCI帧。

1. 蓝牙OPP架构

本章根据BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 1, Part A2的图2.1——蓝牙核心系统架构,给出OPP的架构。

Message Access Profile (MAP) 和Phone Book Access Profile (PBAP) 这两个profile和Object Push Profile (OPP) 是比较类似的profile。下面会根据Bluetooth SIG的文档,简单地看看几个Profile。

1.1 OPP

文档参考https://www.bluetooth.com/specifications/specs/object-push-profile-1-2-1/

对象推送配置文件 (OPP) 定义了提供对象推送使用模型的应用程序应使用的协议和程序的要求。该配置文件利用通用对象交换配置文件 (GOEP) 来定义应用程序所需协议的互操作性要求。使用这些模型的常见设备是笔记本电脑、PDA 和移动电话。

此配置文件涵盖的场景如下: 

  • 使用蓝牙设备将对象推送到另一个蓝牙设备的收件箱。例如,该对象可以是名片或约会。
  • 使用蓝牙设备从另一个蓝牙设备中提取名片。
  • 使用蓝牙设备与另一个蓝牙设备交换名片。交换被定义为名片的推送,然后是名片的拉动。

下图1.1为OPP的蓝牙配置文件结构。

 下图2.1为OPP的协议栈模型。

 下图2.2为OPP配置和角色。

 OPP角色如下:

  • 推送服务器 - 此设备提供对象交换服务器。除了本配置文件中定义的互操作性要求外,如果没有相反定义,Push Server 还应符合 GOEP 服务器的互操作性要求。
  • 推送客户端——该设备向推送服务器推送和拉取对象。除了本配置文件中定义的互操作性要求外,如果没有相反定义,Push 客户端还应符合 GOEP 客户端的互操作性要求。

1.2 PBAP

文档参考Phone Book Access Profile 1.2.3 – Bluetooth® Technology Website

电话簿访问配置文件 (PBAP) 定义了设备应用于检索电话簿对象的协议和程序。它基于客户端设备从服务器设备中提取电话簿对象的客户端-服务器交互模型。

此配置文件是专门为免提使用案例量身定制的(即,与“免提配置文件(Hands-Free Profile)”或“SIM 访问配置文件(SIM Access Profile)”结合实施)。它提供了许多功能,可以根据汽车环境的需要对电话簿对象进行高级处理。特别是,它比对象推送配置文件(Object Push Profile ,可用于将 vCard 格式的电话簿条目从一台设备推送到另一台设备)要丰富得多。此配置文件还可以应用于客户端设备从服务器设备中提取电话簿对象的其他用例。但请注意,此配置文件仅允许查询电话簿对象(只读)。无法更改原始电话簿对象的内容(读/写)。

电话簿访问配置文件依赖于通用对象交换配置文件(GOEP)、串行端口配置文件(SPP)和通用访问配置文件(GAP)。

下图为PBAP配置栈,Baseband、LMP 和 L2CAP 是蓝牙协议的物理和数据链路层。 RFCOMM 是蓝牙串口仿真实体。 SDP 是蓝牙服务发现协议。

 PBAP 会话被定义为客户端和服务器之间的底层 OBEX 连接,使用 PBAP 目标 UUID。

L2CAP 互操作性要求在 GOEP v2.0 或更高版本中定义。 PBAP v1.2 配置文件需要与基于 GOEP v1.1 的先前版本的 PBAP 向后兼容。应使用 GOEP v2.0 或更高版本第 6.2 节中定义的向后兼容程序。当使用 OBEX over RFCOMM (GOEP v1.1) 时,只应使用特征的一个子集。在 SDP 条目中通告了对 GOEP1.1 和 GOEP2.0(或更高版本)的支持。

下图为PBAP的配置和角色。

 PBAP角色为:

  • 电话簿服务器设备 (PSE) – 这是包含源电话簿对象的设备。
  • 电话簿客户端设备 (PCE) – 这是从服务器设备检索电话簿对象的设备。

1.3 MAP

文档参考https://www.bluetooth.com/specifications/specs/message-access-profile-1-4/

消息访问配置文件 (MAP) 规范定义了一组用于在设备之间交换消息的功能和过程。

它专为车载终端设备(通常是安装在汽车中的 Car-Kit)利用通信设备(通常是手机)的消息传递功能的车载免提用例量身定制。但是,此配置文件也可用于需要在两个设备之间交换消息的其他用例。

消息访问配置文件 (MAP) 定义了交换消息对象的设备应使用的特性和过程。它基于客户端发起事务的客户端-服务器交互模型。

MAP配置如下图所示:

MAP依赖于GOEP、SPP和GAP。与 GOEP 1.1 设备交互时需要SPP。此配置文件旨在与电话簿访问配置文件 (PBAP) v1.2 或更高版本的并置实现共享联系信息。

MAP协议栈

 MAS = 消息访问服务,MNS = 消息通知服务(参见第 5.10 节); bMessages 是 MAP 用于消息传输的应用程序对象(参见第 3 节)。 L2CAP 互操作性要求在 GOEP v2.0 或更高版本 [24] 中定义。 MAP v1.2 配置文件需要向后兼容基于 GOEP v1.1 的以前版本的 MAP。应使用 GOEP v2.0 或更高版本的第 6.2 节中定义的向后兼容程序。两个设备之间的所有底层 MAP OBEX 连接(MAS 和 MNS)当时都应使用单一版本的 GOEP。当使用 OBEX over RFCOMM (GOEP v1.1) 时,只应使用特征的一个子集。在 SDP 条目中通告了对 GOEP v1.1 和/或 GOEP v2.0 或更高版本的支持。

MAP角色如下:

  • 消息服务器设备 (MSE) – 是提供消息存储库引擎的设备(即,能够向客户端单元提供存储在此设备中的消息以及其消息存储库中的更改通知)。 
  • 消息客户端设备 (MCE) – 是使用 MSE 的消息存储库引擎浏览和显示现有消息并将 MCE 上创建的消息上传到 MSE 的设备。

MAP应用场景

(1)免提配置:在图 2.2 中,汽车中的免提单元接收/发送来自/发送到手机的消息,它提供了网络访问和消息存储的功能(Hands-Free Use Case)。

(2)PC 用例:图 2.3 显示了将 PC 用作 MAP 客户端的配置,以便用户能够将其 PC 用作存储在手机中的消息的 IO 设备。

在任何情况下,手机都充当 MSE,而其他设备则充当 MCE 角色。

1.4 GOEP

文档参考Generic Object Exchange Profile 2.0 – Bluetooth® Technology Website

通用对象交换配置(GOEP)文件定义了应用程序应使用的协议和过程,这些应用程序提供了需要对象交换能力的使用模型。例如,使用模型可以是同步、文件传输或对象推送模型。

GOEP配置结构如下图1.1所示。

GOEP协议栈模型如下所示。

2. OPP Code Flow

本章以一台Android手机向另一台Android手机发送一张图片为例。

下图为Push Client的代码调用流程,读者可以自行去绘制Push Server的代码调用流程。

首先,开机后会启动BluetoothOppService,这部分内容在Android Bluetooth启动_阅后即奋的博客-CSDN博客_android bluetooth的流程图中可以看到。

OppService启动时,会注册

    @Override
    protected void create() {
        if (D) {
            Log.v(TAG, "onCreate");
        }
        final ContentResolver contentResolver = getContentResolver();
        new Thread("trimDatabase") {
            @Override
            public void run() {
                trimDatabase(contentResolver);
                mHandler.sendMessage(mHandler.obtainMessage(MSG_START_UPDATE_THREAD));
            }
        }.start();
// ......
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG, " handleMessage :" + msg.what);
            switch (msg.what) {
            // ......
                case MSG_START_UPDATE_THREAD:
                    mObserver = new BluetoothShareContentObserver();
                    getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI,
                            true, mObserver);

                    mNotifier = new BluetoothOppNotification(BluetoothOppService.this);
                    mNotifier.mNotificationMgr.cancelAll();
                    mNotifier.updateNotification();

                    updateFromProvider();
                    break;
            // ......
        }
    }

然后,在Phone A上吗,选择一张本地的图片,通过蓝牙的方式分享,找到对端设备Phone B的蓝牙名称,并选中发送。

注:部分OEM厂商,为了实现高速传输文件的功能,会通过P2P代替OPP,建议将Phone A和Phone B的WIFI开关关闭后,再去做上述分享文件的操作。

当对端设备选中后,会收到action为BluetoothDevicePicker.ACTION_DEVICE_SELECTED的广播。

public class BluetoothOppReceiver extends 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(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) {
// ......
            // Insert transfer session record to database
            mOppManager.startTransfer(remoteDevice);
// ......
}

由于本例为单个文件分享,创建的线程会执行到insertSingleShare()方法。

public class BluetoothOppManager {
// ......
    public void startTransfer(BluetoothDevice device) {
//......
            insertThread = new InsertShareInfoThread(device, mMultipleFlag, mMimeTypeOfSendingFile,
                    mUriOfSendingFile, mMimeTypeOfSendingFiles, mUrisOfSendingFiles,
                    mIsHandoverInitiated);
            if (mMultipleFlag) {
                mFileNumInBatch = mUrisOfSendingFiles.size();
            }
        }

        insertThread.start();
    }

    private class InsertShareInfoThread extends Thread {
//......

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

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

ContentResolver通过Uri访问应用程序的ContentProvider,实现增删查改。

数据库插入成功后,BluetoothOppService注册的Observer就可以收到回调,去执行updateFromProvider()方法,创建线程去执行发送任务。

    private void updateFromProvider() {
        synchronized (BluetoothOppService.this) {
            mPendingUpdate = true;
            if (mUpdateThread == null) {
                mUpdateThread = new UpdateThread();
                mUpdateThread.start();
                mUpdateThreadRunning = true;
            }
        }
    }


    private class UpdateThread extends Thread {

        @Override
        public void run() {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            while (!mIsInterrupted) {

                while (!isAfterLast || arrayPos < mShares.size() && mListenStarted) {
                    if (isAfterLast) {
                        // ......
                    } else {
                        int id = cursor.getInt(idColumn);

                        if (arrayPos == mShares.size()) {
                            insertShare(cursor, arrayPos);
                            if (V) {
                                Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
                            }
                            scanFileIfNeeded(arrayPos);
                            ++arrayPos;
                            cursor.moveToNext();
                            isAfterLast = cursor.isAfterLast();
                        } else {
                            // ......
                    }
                }

                mNotifier.updateNotification();

                cursor.close();
            }

            mUpdateThreadRunning = false;
        }
    }

    private void insertShare(Cursor cursor, int arrayPos) {
// ......
                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();
                }
//......
    }

BluetoothOppTransfer首先会判断Controller是否支持OBEX,如果SDP查找OBEX失败,就会直接通过RFCOMM来创建连接。

public class BluetoothOppTransfer implements BluetoothOppBatch.BluetoothOppBatchListener {
// ......
    public void start() {
        // ......
        // 注册广播接收器
        registerConnectionreceiver();
        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();
            }
        }

    }

    private void registerConnectionreceiver() {
        /*
         * OBEX channel need to be monitored for unexpected ACL disconnection
         * such as Remote Battery removal
         */
        synchronized (this) {
            try {
                if (mBluetoothReceiver == null) {
                    mBluetoothReceiver = new OppConnectionReceiver();
                    IntentFilter filter = new IntentFilter();
                    filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
                    filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
                    mContext.registerReceiver(mBluetoothReceiver, filter);
                    if (V) {
                        Log.v(TAG, "Registered mBluetoothReceiver");
                    }
                }
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "mBluetoothReceiver Registered already ", e);
            }
        }
    }

    private void startConnectSession() {
        mDevice = mBatch.mDestination;
        // SDP查找OBEX service
        if (!mBatch.mDestination.sdpSearch(BluetoothUuid.OBEX_OBJECT_PUSH)) {
            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();
        }
    }

// ......

}

Sdp search的结果会在JNI中收到回调。

// packages/apps/Bluetooth/jni/com_android_bluetooth_sdp.cpp
static void sdp_search_callback(bt_status_t status, const RawAddress& bd_addr,
                                const Uuid& uuid_in, int count,
                                bluetooth_sdp_record* records) {
// ......
    else if (uuid_in == UUID_OBEX_OBJECT_PUSH) {
      jint formats_list_size = record->ops.supported_formats_list_len;
      ScopedLocalRef<jbyteArray> formats_list(
          sCallbackEnv.get(), sCallbackEnv->NewByteArray(formats_list_size));
      if (!formats_list.get()) return;
      sCallbackEnv->SetByteArrayRegion(
          formats_list.get(), 0, formats_list_size,
          (jbyte*)record->ops.supported_formats_list);

      sCallbackEnv->CallVoidMethod(
          sCallbacksObj, method_sdpOppOpsRecordFoundCallback, (jint)status,
          addr.get(), uuid.get(), (jint)record->ops.hdr.l2cap_psm,
          (jint)record->ops.hdr.rfcomm_channel_number,
          (jint)record->ops.hdr.profile_version, service_name.get(),
          formats_list.get(), more_results);

    }
// ......
}

static void classInitNative(JNIEnv* env, jclass clazz) {
  /* generic SDP record (raw data)*/
  method_sdpRecordFoundCallback =
      env->GetMethodID(clazz, "sdpRecordFoundCallback", "(I[B[BI[B)V");

  /* MAS SDP record*/
  method_sdpMasRecordFoundCallback = env->GetMethodID(
      clazz, "sdpMasRecordFoundCallback", "(I[B[BIIIIIILjava/lang/String;Z)V");
  /* MNS SDP record*/
  method_sdpMnsRecordFoundCallback = env->GetMethodID(
      clazz, "sdpMnsRecordFoundCallback", "(I[B[BIIIILjava/lang/String;Z)V");
  /* PBAP PSE record */
  method_sdpPseRecordFoundCallback = env->GetMethodID(
      clazz, "sdpPseRecordFoundCallback", "(I[B[BIIIIILjava/lang/String;Z)V");
  /* OPP Server record */
  method_sdpOppOpsRecordFoundCallback =
      env->GetMethodID(clazz, "sdpOppOpsRecordFoundCallback",
                       "(I[B[BIIILjava/lang/String;[BZ)V");
  /* SAP Server record */
  method_sdpSapsRecordFoundCallback = env->GetMethodID(
      clazz, "sdpSapsRecordFoundCallback", "(I[B[BIILjava/lang/String;Z)V");
  /* DIP record */
  method_sdpDipRecordFoundCallback = env->GetMethodID(
      clazz, "sdpDipRecordFoundCallback", "(I[B[BIIIIIZZ)V");
}
// packages/apps/Bluetooth/src/com/android/bluetooth/sdp/SdpManager.java
    void sdpOppOpsRecordFoundCallback(int status, byte[] address, byte[] uuid, int l2capPsm,
            int rfcommCannelNumber, int profileVersion, String serviceName, byte[] formatsList,
            boolean moreResults) {
            // ......
            sendSdpIntent(inst, sdpRecord, moreResults);
        }
    }

    /* Caller must hold the mTrackerLock */
    private void sendSdpIntent(SdpSearchInstance inst, Parcelable record, boolean moreResults) {

        inst.stopSearch();

        Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD);

        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, inst.getDevice());
        intent.putExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, inst.getStatus());
        if (record != null) {
            intent.putExtra(BluetoothDevice.EXTRA_SDP_RECORD, record);
        }
        intent.putExtra(BluetoothDevice.EXTRA_UUID, inst.getUuid());
        /* TODO:  BLUETOOTH_ADMIN_PERM was private... change to callback interface.
         * Keep in mind that the MAP client needs to use this as well,
         * hence to make it call-backs, the MAP client profile needs to be
         * part of the Bluetooth APK. */
        // 发送广播,action为BluetoothDevice.ACTION_SDP_RECORD
        sAdapterService.sendBroadcast(intent, BLUETOOTH_CONNECT,
                Utils.getTempAllowlistBroadcastOptions());

        if (!moreResults) {
            //Remove the outstanding UUID request
            sSdpSearchTracker.remove(inst);
            sSearchInProgress = false;
            startSearch();
        }
    }

在之前的分析中,我们看到了BluetoothOppTransfer注册了广播接收器

// packages/apps/Bluetooth/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
    private class OppConnectionReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (D) {
                Log.d(TAG, " Action :" + action);
            }
            // ......
            else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
                ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
                if (D) {
                    Log.d(TAG, "Received UUID: " + uuid.toString());
                    Log.d(TAG, "expected UUID: " + BluetoothUuid.OBEX_OBJECT_PUSH.toString());
                }
                if (uuid.equals(BluetoothUuid.OBEX_OBJECT_PUSH)) {
                    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;
                }
            }
        }
    }

    private class SocketConnectThread extends Thread {
// ......
        @Override
        public void run() {
            mTimestamp = System.currentTimeMillis();
            if (D) {
                Log.d(TAG, "sdp initiated = " + mSdpInitiated + " l2cChannel :" + mL2cChannel);
            }
            // check if sdp initiated successfully for l2cap or not. If not
            // connect
            // directly to rfcomm
            if (!mSdpInitiated || mL2cChannel < 0) {
                /* sdp failed for some reason, connect on rfcomm */
                Log.d(TAG, "sdp not initiated, connecting on rfcomm");
                connectRfcommSocket();
                return;
            }

            /* Reset the flag */
            mSdpInitiated = false;

            /* Use BluetoothSocket to connect */
            try {
                if (mIsInterrupted) {
                    Log.e(TAG, "btSocket connect interrupted ");
                    markConnectionFailed(mBtSocket);
                    return;
                } else {
                    // 创建BT socket
                    mBtSocket = mDevice.createInsecureL2capSocket(mL2cChannel);
                }
            } catch (IOException e1) {
                Log.e(TAG, "L2cap socket create error", e1);
                connectRfcommSocket();
                return;
            }
            try {
                // 连接服务端Socket
                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());
                }
                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;
            }
        }
    }

// ......
    }

    /*
     * Receives events from mConnectThread & mSession back in the main thread.
     */
    private class EventHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.i(TAG, " handleMessage :" + msg.what);
            switch (msg.what) {
                case TRANSPORT_CONNECTED:
                    /*
                    * RFCOMM connected is for outbound share only! Create
                    * BluetoothOppObexClientSession and start it
                    */
                    if (V) {
                        Log.v(TAG, "Transfer receive TRANSPORT_CONNECTED msg");
                    }

                    mTransport = (ObexTransport) msg.obj;
                    // 开始Obex Session
                    startObexSession();
                    break;
            }
        }
    }

后面的流程可以参考流程图,通过BluetoothSocket将request发送到L2CAP层。

3. 日志分析

日志附件:https://download.csdn.net/download/hihan_5/86743876

3.1 logcat

// 收到广播,action为android.bluetooth.devicepicker.action.DEVICE_SELECTED,表示选中了对端设备。
09-21 10:44:46.128  1002 11204 11204 D BluetoothOppReceiver:  action :android.bluetooth.devicepicker.action.DEVICE_SELECTED
09-21 10:44:46.128  1002 11204 11204 D BluetoothOppReceiver: Received BT device selected intent, bt device: F0:6C:5D:C5:33:59
09-21 10:44:46.178  1002 11204 12272 D BtOppService: insertShare parsed URI: content://com.android.fileexplorer.myprovider/external_files/DCIM/Screenshots/Screenshot_2022-09-21-10-42-55-060_com.miui.home.jpg@2aa5c2c

// 从Map中,根据Uri取出BluetoothOppSendFileInfo对象
09-21 10:44:46.178  1002 11204 12272 D BluetoothOppUtility: getSendFileInfo: uri=content://com.android.fileexplorer.myprovider/external_files/DCIM/Screenshots/Screenshot_2022-09-21-10-42-55-060_com.miui.home.jpg@2aa5c2c

// msg.what == MSG_SEND_READY
09-21 10:44:46.183  1002 11204 11204 I BtOppService:  handleMessage :601
09-21 10:44:46.230  1002 11204 11204 D BtOppService: start opp

// msg.what == MSG_START_OPP_TRANSFER
09-21 10:44:46.230  1002 11204 11204 I BtOppService:  handleMessage :603
09-21 10:44:46.230  1002 11204 11204 D BtOppService: Service start transfer new Batch 1

// startSearch(),UUID is OBEX_OBJECT_PUSH
09-21 10:44:46.240  1002 11204 11204 D SdpManager: Starting search for UUID: 00001105-0000-1000-8000-00805f9b34fb

// 连接上对端设备
09-21 10:44:46.622  1002 11204 11204 D BtOppService: action : android.bluetooth.device.action.ACL_CONNECTED
09-21 10:44:46.622  1002 11204 11204 D BtOppService: device connected: F0:6C:5D:C5:33:59
09-21 10:44:46.622  1002 11204 11204 I BtOppService:  handleMessage :618

// sdp search callback
09-21 10:44:46.742  1002 11204 11244 D SdpManager: UUID: [0, 0, 17, 5, 0, 0, 16, 0, -128, 0, 0, -128, 95, -101, 52, -5]
09-21 10:44:46.743  1002 11204 11244 D SdpManager: UUID in parcel: 00001105-0000-1000-8000-00805f9b34fb
09-21 10:44:46.744  1002 11204 11244 D SdpManager: startSearch(): nextInst = null mSearchInProgress = false - search busy or queue empty.
09-21 10:44:46.746  1002 11204 11204 D BtOppTransfer:  Action :android.bluetooth.device.action.SDP_RECORD
09-21 10:44:46.746  1002 11204 11204 D BtOppTransfer: Received UUID: 00001105-0000-1000-8000-00805f9b34fb
09-21 10:44:46.746  1002 11204 11204 D BtOppTransfer: expected UUID: 00001105-0000-1000-8000-00805f9b34fb
09-21 10:44:46.746  1002 11204 11204 D BtOppTransfer:  -> status: 0

// 在创建的线程中,开始传输任务
09-21 10:44:46.747  1002 11204 12318 D BtOppTransfer: sdp initiated = true l2cChannel :4131
09-21 10:44:48.621  1002 11204 12318 D BluetoothSocket: startRecord connect mPfd = {ParcelFileDescriptor: java.io.FileDescriptor@cfc06c5}fd = 145
09-21 10:44:48.621  1002 11204 12318 D BluetoothSocketStubImpl: startRecord fd = 145
09-21 10:44:48.624  1002 11204 12318 V BtOppTransfer: L2cap socket connection attempt took 1877 ms
// msg.what == TRANSPORT_CONNECTED --> startObexSession()
09-21 10:44:48.630  1002 11204 12278 I BtOppTransfer:  handleMessage :11
09-21 10:44:48.631  1002 11204 12278 D BtOppObexClient: Start!

// 获取BluetoothOppSendFileInfo对象
09-21 10:44:48.632  1002 11204 12278 D BluetoothOppUtility: getSendFileInfo: uri=content://com.android.fileexplorer.myprovider/external_files/DCIM/Screenshots/Screenshot_2022-09-21-10-42-55-060_com.miui.home.jpg@2aa5c2c

// connect 
09-21 10:44:48.736  1002 11204 12430 D BtOppObexClient: Create ClientSession with transport com.android.bluetooth.BluetoothObexTransport@3ee9072
09-21 10:44:48.812  1002 11204 12430 D BtOppObexClient: OBEX session created

// 对端接受文件传输请求
09-21 10:44:53.076  1002 11204 12430 V BtOppObexClient: Remote accept

// 发送成功
09-21 10:44:56.505  1002 11204 12430 I BtOppObexClient: SendFile finished send out file Screenshot_2022-09-21-10-42-55-060_com.miui.home.jpg length 1004093
09-21 10:44:56.505  1002 11204 12430 D BtOppUtils:  Approx. throughput is 2348 Kbps
09-21 10:44:57.866  1002 11204 12430 D BluetoothOppUtility: closeSendFileInfo: uri=content://com.android.fileexplorer.myprovider/external_files/DCIM/Screenshots/Screenshot_2022-09-21-10-42-55-060_com.miui.home.jpg@2aa5c2c
09-21 10:44:57.868  1002 11204 12430 V BtOppObexClient: Get response code 160
09-21 10:44:57.883  1002 11204 12430 D BtOppObexClient: Client thread waiting for next share, sleep for 500
09-21 10:44:57.883  1002 11204 12278 I BtOppTransfer:  handleMessage :0
09-21 10:44:57.883  1002 11204 12278 D BtOppObexClient: Stop!
09-21 10:44:57.883  1002 11204 11204 D BluetoothOppReceiver:  action :android.btopp.intent.action.TRANSFER_COMPLETE
09-21 10:44:58.013  1002 11204 12430 D BtOppObexClient: OBEX session disconnected
09-21 10:44:58.014  1002 11204 12430 D BluetoothSocket: close() this: android.bluetooth.BluetoothSocket@38596c, channel: 4131, mSocketIS: android.net.LocalSocketImpl$SocketInputStream@8c52b35, mSocketOS: android.net.LocalSocketImpl$SocketOutputStream@f1bbbcamSocket: android.net.LocalSocket@a42b13b impl:android.net.LocalSocketImpl@f07a058 fd:java.io.FileDescriptor@cfc06c5, mSocketState: CONNECTED
09-21 10:44:58.014  1002 11204 12430 D BluetoothSocket: startRecord close mPfd = {ParcelFileDescriptor: java.io.FileDescriptor@cfc06c5}fd = 145
09-21 10:44:58.014  1002 11204 12430 D BluetoothSocketStubImpl: startRecord fd = 145
09-21 10:44:58.019  1002 11204 12278 I BtOppTransfer:  handleMessage :1
09-21 10:44:58.026  1002 11204 12553 D BtOppService: batch.mStatus:2
09-21 10:44:58.026  1002 11204 12553 V BtOppTransfer: stop
09-21 10:44:58.026  1002 11204 12553 D BtOppObexClient: Stop!
09-21 10:44:58.027  1002 11204 12553 D BtOppTransfer: start interrupt :android.bluetooth.BluetoothSocket@38596c
09-21 10:44:58.028  1002 11204 12553 D BluetoothSocket: close() this: android.bluetooth.BluetoothSocket@38596c, channel: 4131, mSocketIS: android.net.LocalSocketImpl$SocketInputStream@8c52b35, mSocketOS: android.net.LocalSocketImpl$SocketOutputStream@f1bbbcamSocket: null, mSocketState: CLOSED
09-21 10:44:58.028  1002 11204 12553 V BtOppTransfer: waiting for connect thread to terminate
09-21 10:44:58.028  1002 11204 12553 D BtOppTransfer: mConnectThread terminated
09-21 10:44:58.028  1002 11204 12553 V BtOppService: Remove batch 1
09-21 10:45:02.237  1002 11204 11204 D BtOppService: action : android.bluetooth.device.action.ACL_DISCONNECTED
09-21 10:45:02.237  1002 11204 11204 D BtOppService: device disconnected: F0:6C:5D:C5:33:59
09-21 10:45:02.237  1002 11204 11204 I BtOppService:  handleMessage :619

3.2 HCI日志

本节会讲到SDP,L2CAP和OBEX的一些基础概念,其实这些内容单独拆成一篇blog也是大篇幅的文章,本篇讲到它们是想要结合一些HCI日志,方便读者理解。

3.2.1 SDP

基于BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part B SERVICE DISCOVERY PROTOCOL (SDP) SPECIFICATION。

服务发现机制为客户端应用程序提供了发现服务器应用程序提供的服务的存在以及这些服务的属性的方法。服务的属性包括所提供服务的类型或类别以及使用该服务所需的机制或协议信息。

SDP涉及SDP服务器和SDP客户端之间的通信。服务器维护一个 SDP 数据库,该数据库由描述与服务器相关的服务特征的服务记录列表组成。每个服务记录都包含有关单个服务的信息。客户端可以通过发出 SDP 请求从 SDP 服务器维护的服务记录中检索信息。

如果客户端或与客户端关联的应用程序决定使用服务,它会打开与服务提供者的单独连接以使用该服务。 SDP 提供了一种发现服务及其属性的机制(包括相关的服务访问协议),但它不提供使用这些服务的机制(例如交付服务访问协议)。

 Client发送一个requests到Server,Server返回一个responses。

首先HCI日志的Message Sequence Chart(MSC),如下图所示,Client发送Frame#3374,搜索“OBEX”是否支持,Server回复了一个response,告知client支持OBEX,version为v1.2,RFCOMM channel为12。

request Frame#3374,Packet为ACL Data Packet,L2CAP的Channel ID为0x0040,为SDP开放。

 基于BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part B4.2,图4.1为其PDU格式。

response Frame#3377如下所示,Packet为ACL Data Packet,L2CAP的Channel ID为0x0040。

 上述所述的PDU ID在BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part B4.2中可以看到定义。

TransactionID的要求是response保持与request一致即可,request提供的TransactionID与所有的未完成的requests的TransactionID一致即可。

 ParameterLength的长度,是指所有Parameters的长度。

 更加详细的SERVICESEARCHATTRIBUTE TRANSACTION可以看BLUETOOTH CORE SPECIFICATION Version 5.2 | Vol 3, Part B4.7,

(1)SDP_SERVICE_SEARCH_ATTR_REQ

SDP_SERVICE_SEARCH_ATTR_REQ 事务将 SDP_SERVICE_SEARCH_REQ 和 SDP_SERVICE_ATTR_REQ 的功能组合成一个请求。作为参数,它包含服务搜索模式和要从与服务搜索模式匹配的服务记录中检索的属性列表。 SDP_SERVICE_SEARCH_ATTR_REQ 及其响应更复杂,可能需要比单独的 SDP_ServiceSearch 和 SDP_ServiceAttribute 事务更多的字节。但是,使用 SDP_SERVICE_SEARCH_ATTR_REQ 可以减少 SDP 事务的总数,尤其是在检索多个服务记录时。每个服务记录的服务记录句柄包含在该服务的 ServiceRecordHandle 属性中,并且可以与其他属性一起请求。 

下图为SDP_SERVICE_SEARCH_ATTR_REQ事务,包含4个参数:

第一个参数: ServiceSearchPattern。按照描述,ServiceSearchPattern 是一个数据元素序列,其中序列中的每个元素都是一个 UUID。

 第二个参数:MaximumAttributeByteCount。按照描述,MaximumAttributeByteCount 指定在响应此请求时要返回的属性数据的最大字节数。 

第三个参数:AttributeIDList。按照描述,AttributeIDList 是一个数据元素序列,其中列表中的每个元素要么是一个属性 ID,要么是一个属性 ID 范围。

 第四个参数:ContinuationState。按照描述,ContinuationState 由一个 8 bit数字 N 组成,该字节数表示继续状态信息的字节数,后跟 N 个字节的继续状态信息,这些字节在服务器的先前响应中返回。 

(2) SDP_SERVICE_SEARCH_ATTR_RSP PDU

SDP 服务器在收到有效的 SDP_SERVICE_SEARCH_ATTR_REQ 后生成 SDP_SERVICE_SEARCH_ATTR_RSP。响应包含来自与请求的服务搜索模式匹配的服务记录的属性列表(属性 ID 和属性值)。

下图为SDP_SERVICE_SEARCH_ATTR_RSP事务,包含了3个参数。

 第一个参数:AttributeListsByteCount。按照描述,AttributeListsByteCount 包含 AttributeLists 参数中的字节数。 N 不得大于 SDP_SERVICE_SEARCH_ATTR_REQ 中指定的 MaximumAttributeByteCount 值。

第二个参数:AttributeLists。按照描述,AttributeLists 是一个数据元素序列,其中每个元素又是一个表示属性列表的数据元素序列。每个属性列表都包含来自一个服务记录的属性 ID 和属性值。只有在服务记录中具有非空值且其属性 ID 在 SDP_SERVICE_SEARCH_ATTR_REQ 中指定的属性才包含在 AttributeLists 中。对于服务记录中没有值的属性,属性 ID 和属性值都不会放在 AttributeLists 中。

 第三个参数:ContinuationState。按照描述,ContinuationState 由一个 8 bit N 组成,即连续状态信息的字节数,后跟 N 个字节的连续信息。

(3) 实例分析

结合上面Frame#3374,Frame#3377,和上述Core Specification所述,HCI日志就很好看懂了,对应红箭头所指各部分。

3.2.2 L2CAP

参考Bluetooth L2CAP_阅后即奋的博客-CSDN博客

实例分析

本例中,L2CAP先给SDP分配了0x0040的CID,后给OBEX分配了0x0041的CID。

 根据Trans.ID可以区分传输的流程。这里Trans.ID=4的看Frame#3351,Frame#3362,Frame#3366,Frame#3368;Trans.ID=5的看Frame#3364,Frame#3373。

首先看Trans.ID=4的Frame。

Frame#3351为Master发给Slave的CID=0x0001的信令,Code为L2CAP_CONNECTION_REQ(即0x02),PSM为SDP即0x0001,SCID为0x0040。

Frame#3362为Slave发给Master的CID=0x0001的信令,Code为L2CAP_CONNECTION_RSP(即0x03),DCID为0x0040,SCID为0x0040,Result为Connection successful(即0x0000)。

Frame#3366为Slave发给Master的CID=0x0001的信令,Code为L2CAP_CONFIGURATION_REQ
(即0x04)。

Frame#3368为Master发给Slave的CID=0x0001的信令,Code为L2CAP_CONFIGURATION_RSP
(即0x05),Result为success(即0x0000)。

再看Trans.ID=5的Frame。

Frame#3364为Master发给Slave的CID=0x0001的信令,Code为L2CAP_CONFIGURATION_REQ
(即0x04)。

Frame#3373为Slave发给Master的CID=0x0001的信令,Code为L2CAP_CONFIGURATION_RSP
(即0x05),Result为success(即0x0000)。

这里我们可以发现,L2CAP_CONFIGURATION_REQ这个请求Master和Slave都主动发出过,为什么都要发一次呢?

其实是为了交换彼此的CONFIGURATION PARAMETER OPTIONS。本例中,双方交换了MTU能力,此选项指定发送方能够接受的通道的最大 SDU 大小。

3.2.3 OBEX

OBEX的具体内容在Core Spec中已经移除掉了,放在了各个使用OBEX协议的Profile文档中了,例如OPP,MAP,PHAP,GOEP。

本文就参考OPP中的OBEX部分,进行分析。

OBEX的操作有如下几种:

(1)Initialization of OBEX

由于此配置文件不使用 OBEX 身份验证,因此 OBEX 初始化不适用。

(2)Establishment of OBEX connection

请参阅 GOEP 中的第 5.4.1 节,了解无需身份验证的 OBEX 连接建立的描述。 Push Client 在建立 OBEX 连接时不使用目标头。

 Connect操作请求中的字段如下:

 Connect操作请求的response的字段如下:

(3)Pushing Data

Push Client 在将对象推送到 Push Server 时应该使用 Type Header。如果发送时没有 Type Header,Push Server 应该接受支持的对象。

Server可以根据 GOEP 中定义的“用户接受”场景发送单响应模式参数 (SRMP) 标头。

请参阅 GOEP 中的第 5.5 节。

使用 OBEX 协议的 PUT 操作将数据对象推送到服务器。数据可以在一个或多个 OBEX 数据包中发送。

客户端可以通过发送SRMP Header来启动 SRM。 SRM 允许客户端发出未确认的请求数据包,直到 PUT 操作完成。如果服务器通过可选的 SRMP Header请求,则可以在 PUT 操作期间发出额外的响应数据包(有关 SRMP 使用要求,请参见GOEP文档第 4.6 节)

如果 PUT 操作正在进行,并且服务器在接收到初始 Body 标头后无法接收更多 Body 数据,并且启用了 SRM,则服务器可能会在整个过程之前发送包含不成功响应代码(不包括 CONTINUE 和 SUCCESS)的 PUT 响应服务器接收到的对象。或者,在这种情况下,服务器可以选择断开传输而不等待 PUT 操作完成。如果服务器在没有等待对象的最后一个块的情况下发送了一个 PUT 响应,则它不应在包含 Final Bit 的 PUT 请求中的整个对象到达时发送另一个响应。

如果客户端在 SRM 操作的中间收到 PUT 响应(SUCCESS 或 CONTINUE 除外),它可能会执行以下三种操作之一:1)停止处理 PUT 操作,2)完成其排队的 PUT 数据包,然后停止处理 PUT操作,或 3) 完成 PUT 操作,然后处理早期响应数据包。在所有情况下,服务器都应删除任何附加的正文数据,直到操作完成。当服务器接收到另一个指示新操作开始的 nonBody 标头时,服务器就知道该操作已完成。此非正文标头不包括在可靠会话期间在所有 OBEX 数据包中到达的会话序列号 (SSN) 标头。

如果客户端收到带有非预期的 SUCCESS 或 CONTINUE 响应代码的早期 PUT 响应,则传输应断开。

第一个 PUT 请求包括以下字段和标头:

PUT 请求的响应数据包具有以下字段和标头:

本例传文件的操作中,Master使用了几种OBEX操作:Connect、Put、Disconnect。

下图为OBEX相关的Frame,首先是Master和Slave建立连接Frame#3432和Frame#3434,Status都是Final,表示就这一帧即可。后面就是Master向Slave执行Put操作,发出一个Put Frame后要等待Slave返回的Continue Frame才可以继续后续的Put操作。

 当全部内容发生完成,Master发出的最后一次Put Frame#5729的status为final,Slave收到status为final的Frame后,返回Success Frame#5738。Master收到Success Frame后会执行断开OBEX连接的操作。

看前面所述具体帧内容。

Frame#3432,Master发出建立OBEX connect的请求,其Opcode Value为0x80。

Frame#3434,Slave发出建立OBEX connect的响应,其response code为0xA0,表示success。

Frame#3465,Master发送的第一个Put帧,其Length为20,Opcode为Put,包含若干的Header。

Frame#3477,是Slave对Master发来的第一个Put帧的响应,response code为continue,同时包含若干Header。

 Frame#3494,为Master收到salve发来response后,继续进行Put操作发出来的后续帧。

 直到Master发出来最后一个为Final状态的帧。

文件传输结束后,Slave发出response Frame#5738。

 待Master收到Slave的response后,发出disconnect的操作。

 断开连接成功后,Slave再给Master发送一个response。

 至此,整个发送图片的OBEX操作流程结束。

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阅后即奋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值