Android NFC 文件传输

Posted by Vector Blog on July 26, 2018

这里主要来追一下 Android Beam 发送文件的流程

来看一下 Nfc 的 AndroidManifest.xml

        <activity android:name=".BeamShareActivity"
            ......
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND_MULTIPLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
            </intent-filter>

在共享文件选到 Android Beam 时会启动 BeamShareActivity :

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUris = new ArrayList<Uri>();
        mNdefMessage = null;
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        mLaunchIntent = getIntent();
        if (mNfcAdapter == null) {
            ......
        } else {
            // NFC 没打开则先弹出请求打开 NFC 的请求框 
            if (!mNfcAdapter.isEnabled()) {
                showNfcDialogAndExit(com.android.nfc.R.string.beam_requires_nfc_enabled);
            } else {
                parseShareIntentAndFinish(mLaunchIntent);
            }
        }
    }

重点在 parseShareIntentAndFinish

    public void parseShareIntentAndFinish(Intent intent) {
 	......  //从intent 中获取 uri 

	//创建并填充 BeamShareData 变量
        BeamShareData shareData = null;
        UserHandle myUserHandle = new UserHandle(UserHandle.myUserId());
        if (mUris.size() > 0) {
            // Uris have our first preference for sharing
            Uri[] uriArray = new Uri[mUris.size()];
            int numValidUris = 0;
            for (Uri uri : mUris) {
                try {
                    int uid = ActivityManagerNative.getDefault().getLaunchedFromUid(getActivityToken());
                    grantUriPermission("com.android.nfc", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    uriArray[numValidUris++] = uri;
                    if (DBG) Log.d(TAG, "Found uri: " + uri);
                } 
            }
            if (numValidUris != 0 && numValidUris == mUris.size()) {
                shareData = new BeamShareData(null, uriArray, myUserHandle, 0);
            } else {
                // No uris left
                shareData = new BeamShareData(null, null, myUserHandle, 0);
            }
        } ......
        mNfcAdapter.invokeBeam(shareData);
        finish();
    }

最后调用 NfcAdapter 的 invokeBeam 方法, 其直接call 到 NFCService 的 invokeBeamInternal 方法中

        public void invokeBeamInternal(BeamShareData shareData) {
            NfcPermissions.enforceAdminPermissions(mContext);
            Message msg = Message.obtain();
            msg.what = MSG_INVOKE_BEAM;
            msg.obj = shareData;
            mHandler.sendMessageDelayed(msg, INVOKE_BEAM_DELAY_MS);
        }
        
=>           case MSG_INVOKE_BEAM: {
                    mP2pLinkManager.onManualBeamInvoke((BeamShareData)msg.obj);
                    break;
                }        

再来看 P2pLinkManager.java 中的 onManualBeamInvoke 方法

    public void onManualBeamInvoke(BeamShareData shareData) {
        synchronized (P2pLinkManager.this)    {
            ......
            if (mMessageToSend != null ||
                    (mUrisToSend != null && mHandoverDataParser.isHandoverSupported())) {
                mSendState = SEND_STATE_PENDING;
                mEventListener.onP2pNfcTapRequested();
                scheduleTimeoutLocked(MSG_WAIT_FOR_LINK_TIMEOUT, WAIT_FOR_LINK_TIMEOUT_MS);
            }
        }
    }
    
    // 播放start 的声音, 此外截图并显出出 Android Beam 界面
     public void onP2pNfcTapRequested() {
        mNfcService.playSound(NfcService.SOUND_START);
        mNdefSent = false;
        mNdefReceived = false;
        mInDebounce = false;

        mVibrator.vibrate(VIBRATION_PATTERN, -1);
        if (mSendUi != null) {
            mSendUi.takeScreenshot();
            mSendUi.showPreSend(true);     

到此按下分享中 Android Beam 的动作完成,其中设置状态为SEND_STATE_PENDING , 填充要发送文件uri , 并且播放声音以及UI

继续来看一下之后与 remote device 接触后的动作 :
如果对方也支持 P2P , 底层会检测到 NFC_DEP 的接口 :
len = 49 <= 61052E01030580FB0100820202232275182C19869E54056A480000003246666D010112020207FF03020013040164070103
简单解析一下 :
NTF INTF_ACTIVATED Dis_ID:1 , Intf: NFC_DEP , Proto: NFC_DEP , Techo:A_PASSIVE_LISTEN
这里 INTF_ACTIVATED 最终会传个 JNI 层, 忽略中间的一些code :

    public void onLlcpActivated(byte peerLlcpVersion) {
                case LINK_STATE_DOWN:
                    if (mSendState == SEND_STATE_PENDING) {
                        mSendState = SEND_STATE_SENDING;
                        mHandler.removeMessages(MSG_WAIT_FOR_LINK_TIMEOUT);
                        // Immediately try to connect LLCP services
                        connectLlcpServices();
                    }
    void connectLlcpServices() {
        synchronized (P2pLinkManager.this) {
            if (mConnectTask != null) {
                Log.e(TAG, "Still had a reference to mConnectTask!");
            }
            mConnectTask = new ConnectTask();
            mConnectTask.execute();
        }
    }                         

这里新建并启动了一个 ConnectTask :

    final class ConnectTask extends AsyncTask<Void, Void, Boolean> {
        protected Boolean doInBackground(Void... params) {
            synchronized(P2pLinkManager.this) {
                if (mUrisToSend != null) {
                    needsHandover = true;
                }
            if (needsHandover) {
                handoverClient = new HandoverClient();
                    handoverClient.connect();
                    success = true; 
            }    

实质是发送 NFA_P2P_API_CONNECT_EVT 消息给底层来建立连接 :
log : len = 26 => 0000170520061375726E3A6E66633A736E3A68616E646F766572

00	Data  Conn ID:1 
0017	Length
0520	LLCP  Header , DSAP :  01 , Ptype: CONNECT , SAP :20
06(SN ,Service Name ) 13(Len) 75726E3A6E66633A736E3A68616E646F766572 (urn:nfc:sn:handover)

DUT 发起连接, remote device 接受连接请求
log : len = 5<= 000002 8191 Ptype: Connect_Complete, DSAP: 20, SSAP: 11
在 ConnectTask 中会等待连接完成, 之后调用到 onPostExecute

    final class ConnectTask extends AsyncTask<Void, Void, Boolean> {
        @Override
        protected void onPostExecute(Boolean result)  {
                onLlcpServicesConnected();
        }
    }        
            
    void onLlcpServicesConnected() {
            mLlcpServicesConnected = true;
            if (mSendState == SEND_STATE_NEED_CONFIRMATION) {
		.......
            } else if (mSendState == SEND_STATE_SENDING) {
                mEventListener.onP2pResumeSend();
                sendNdefMessage();
            } 
        }
    void sendNdefMessage() {
        synchronized (this) {
            cancelSendNdefMessage();
            mSendTask = new SendTask();
            mSendTask.execute();
        }
    }        

这里又建了一个 SendTask 线程 :

    final class SendTask extends AsyncTask<Void, Void, Void> {
            public Void doInBackground(Void... args) {
            if (uris != null) {
                try {
                    int handoverResult = doHandover(uris, userHandle);
                    switch (handoverResult) {
                        case HANDOVER_SUCCESS:
                            result = true;
                            break;
           }

        int doHandover(Uri[] uris, UserHandle userHandle) throws IOException {
            NdefMessage response = null;
            BeamManager beamManager = BeamManager.getInstance();

            NdefMessage request = mHandoverDataParser.createHandoverRequestMessage();
            if (request != null) {
                if (handoverClient != null) {
                    response = handoverClient.sendHandoverRequest(request);
                }
            }   

这里可以看到先生成一个 HandoverRequestMessage 在发向 remote 发起请求 , 来仔细看一下其过程

    public NdefMessage createHandoverRequestMessage() {
        if (mBluetoothAdapter == null) {
            return null;
        }

        NdefRecord[] dataRecords = new NdefRecord[] {
                createBluetoothOobDataRecord()
        };
        return new NdefMessage(
               createHandoverRequestRecord(),
                dataRecords);
   }

这里会生成一个包含两个 NdefRecord 的 NdefMessage

    // 生成 Handover Request Record
    NdefRecord createHandoverRequestRecord() {
        NdefRecord[] messages = new NdefRecord[] {
                createBluetoothAlternateCarrierRecord(false)
        };

        NdefMessage nestedMessage = new NdefMessage(createCollisionRecord(), messages);

        byte[] nestedPayload = nestedMessage.toByteArray();

        ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length + 1);
        payload.put((byte) 0x12);  // connection handover v1.2
        payload.put(nestedMessage.toByteArray());

        byte[] payloadBytes = new byte[payload.position()];
        payload.position(0);
        payload.get(payloadBytes);
        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, null,
                payloadBytes);
    }
    
//生成包含BT mac address 的 record 
    NdefRecord createBluetoothOobDataRecord() {
        byte[] payload = new byte[8];
        payload[0] = (byte) (payload.length & 0xFF);
        payload[1] = (byte) ((payload.length >> 8) & 0xFF);

        mLocalBluetoothAddress = mBluetoothAdapter.getAddress();

        byte[] addressBytes = addressToReverseBytes(mLocalBluetoothAddress);
        System.arraycopy(addressBytes, 0, payload, 2, 6);

        return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, new byte[]{'b'}, payload);
    }

来看一下实际生成的消息 :
log : len = 73 => 0000464720009102114872129102026372948C5102046163010162005A2008016170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F6262080030EAF0332222

000046	DATA	Len:0x46
4720 	Information, DSAP: 11, SSAP: 20
00	Sequence
91	MB:1 ME:0 SR:1 TNF:RTD 
02	Type Len
11	Payload Len
4872	Type: Hr  RTD Handover Request type
payload 
12  connection handover 版本 v1.2
	//用于解决可能同时发送 HandoverRequest 时出现冲突, 随机数大的一方作为请求者
	91	MB:1 ME:0 SR:1 TNF:RTD
	02	Type Len
	02	Payload Len
	6372	Type: cr  RTD COLLISION RESOLUTION
	948C	两个随机数

	//可选 Carrier 列表 
	51	MB:0  ME:1 TNF:RTD
	02	Type Len
	04	Payload Len
	6163	Type: ac  RTD Alternative Carrier type
	payload 
	01	CARRIER_POWER_STATE_ACTIVE	代表BT是否已经打开
	01	length of carrier data reference	
	62	carrier data reference: ID for Bluetooth OOB data record
	00	Auxiliary data reference count

5A	MB:0  ME:1 SR:1 IL:1 TNF:RF2406 
20	Type Len
08	Payload Len
01	ID len
6170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F62	Type : application/vnd.bluetooth.ep.oob 
62	NDEF ID 'b'
payload 
0800  length
30EAF0332222	BT mac

这个 NDEF 消息中包含两个 NdefRecord , 第一个是 HandoverRequest , 第二个是 Carrier (可以是BT 或者 Wifi ) 的信息。 其中第一个 HandoverRequest record 的payload 实际上又是由一个包含两个 NdefRecord 的 NDEF 消息组成, 一个冲突解决的 record 和 一个列出 Carrier 的 record .

继续来看 remote device 的 response :
log : len = 66 <= 00003F83110191020A487312D102046163010162005A2008016170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F626208005C2809CA2222

00003F
8311	Ptype: Information, DSAP: 20, SSAP: 11	
01		Sequence
91		MB:1 ME:0 SR:1 TNF:RTD 
02		Type Len
0A		Payload Len
4873	Type: Hs  RTD Handover Select  type
Payload
12		connection handover 版本 v1.2
	//Carrier 与 request 中的一致 
	D1	MB=1 ME=1 SR=1 TNF:RTD
	02	Type Len
	04	Payload Len
	6163	Type: ac  RTD Alternative Carrier type
	01016200

5A	MB:0  ME:1 SR:1 IL:1 TNF:RF2406 
20	Type Len
08	Payload Len
01	ID len
6170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F62	Type : application/vnd.bluetooth.ep.oob 
62	NDEF ID 'b'
payload 
0800	length
5C2809CA2222	BT mac

这里 Handover Select Record 后面可以跟随 0 个或者多个 NDEF Record , 表示remote 支持哪些 Carrier , 如果是 0个 则代表remote 不支援 request 中的任何 Carrier .

再来看code 中收到这个消息之后怎么处理 , 这里需要回头在看 sendHandoverRequest , 其在发送之后会一直等待 Select Msg 返回

    public NdefMessage sendHandoverRequest(NdefMessage msg) throws IOException {
        ......
        //发送 HandoverRequest 消息  
        try {
            int remoteMiu = sock.();
            while (offset < buffer.length) {
                int length = Math.min(buffer.length - offset, remoteMiu);
                byte[] tmpBuffer = Arrays.copyOfRange(buffer, offset, offset+length);
                sock.send(tmpBuffer);
                offset += length;
            }

            // 等待 select  消息返回
            byte[] partial = new byte[sock.getLocalMiu()];
            NdefMessage handoverSelectMsg = null;
            while (true) {
                int size = sock.receive(partial);
                if (size < 0) {
                    break;
                }
                byteStream.write(partial, 0, size);
                try {
                    handoverSelectMsg = new NdefMessage(byteStream.toByteArray());
                    // If we get here, message is complete
                    break;
                } catch (FormatException e) {
                    // Ignore, and try to fetch more bytes
                }
            }
            return handoverSelectMsg;
        } finally {
            if (sock != null) {
		    // 在接收到 select 消息之后会断开连接
                    sock.close();
            }
        }            
    }
}

将 handoverSelectMsg 返回之后由前面的 doHandover 继续处理

        int doHandover(Uri[] uris, UserHandle userHandle) throws IOException {
            ......
            if (!beamManager.startBeamSend(mContext,
                    mHandoverDataParser.getOutgoingHandoverData(response), uris, userHandle)) {
                return HANDOVER_BUSY;
            }
            return HANDOVER_SUCCESS;        	

其中 mHandoverDataParser.getOutgoingHandoverData(response) 会从 NDEF 消息中解析出 BT Mac 并创建一个 BluetoothHandoverData 对象

    public boolean startBeamSend(Context context,
                               HandoverDataParser.BluetoothHandoverData outgoingHandoverData,
                               Uri[] uris, UserHandle userHandle) {
                mBeamInProgress = true;

        BeamTransferRecord transferRecord = BeamTransferRecord.forBluetoothDevice(
                outgoingHandoverData.device, outgoingHandoverData.carrierActivating,
                uris);
        Intent sendIntent = new Intent(context.getApplicationContext(),
                BeamSendService.class);
        sendIntent.putExtra(BeamSendService.EXTRA_BEAM_TRANSFER_RECORD, transferRecord);
        sendIntent.putExtra(BeamSendService.EXTRA_BEAM_COMPLETE_CALLBACK,
                new Messenger(mCallback));
        context.startServiceAsUser(sendIntent, userHandle);
        return true;
    }

这里发送广播给 BeamSendService 这个类

public class BeamSendService extends Service implements BeamTransferManager.Callback {
    public int onStartCommand(Intent intent, int flags, int startId) {
        BeamTransferRecord transferRecord;    
        transferRecord = intent.getParcelableExtra(EXTRA_BEAM_TRANSFER_RECORD)   
        mCompleteCallback = intent.getParcelableExtra(EXTRA_BEAM_COMPLETE_CALLBACK);
        if (doTransfer(transferRecord)) {
            return START_STICKY;
        } 
    }
    
    boolean doTransfer(BeamTransferRecord transferRecord) {
        if (createBeamTransferManager(transferRecord)) {
            // register Beam status receiver
            mBeamStatusReceiver = new BeamStatusReceiver(this, mTransferManager);
            registerReceiver(mBeamStatusReceiver, mBeamStatusReceiver.getIntentFilter(),
                    BeamStatusReceiver.BEAM_STATUS_PERMISSION, new Handler());

            if (transferRecord.dataLinkType == BeamTransferRecord.DATA_LINK_TYPE_BLUETOOTH) {
                if (mBluetoothAdapter.isEnabled()) {
                    // Start the transfer
                    mTransferManager.start();
                } else {
                     //如果蓝牙当前没有开启则开启蓝牙, 等待收到开启的广播时会在调用 mTransferManager.start
                    if (!mBluetoothAdapter.enableNoAutoConnect()) {
                        Log.e(TAG, "Error enabling Bluetooth.");
                        mTransferManager = null;
                        return false;
                    }
                    mBluetoothEnabledByNfc = true;
                    if (DBG) Log.d(TAG, "Queueing out transfer "
                            + Integer.toString(transferRecord.id));
                }
            }
            return true;
        }
        return false;
    }    
public class BeamTransferManager implements Handler.Callback,
        MediaScannerConnection.OnScanCompletedListener {
    public void start() {
        mStartTime = System.currentTimeMillis();

        if (!mIncoming) {
            if (mDataLinkType == BeamTransferRecord.DATA_LINK_TYPE_BLUETOOTH) {
                new BluetoothOppHandover(mContext, mRemoteDevice, mUris, mRemoteActivating).start();
            }
        }
    }     

public class BluetoothOppHandover implements Handler.Callback {
    public void start() {
    	//如果 remote 蓝牙未开启则等待一小段时间
        if (mRemoteActivating) {
            Long timeElapsed = SystemClock.elapsedRealtime() - mCreateTime;
            if (timeElapsed < REMOTE_BT_ENABLE_DELAY_MS) {
                mHandler.sendEmptyMessageDelayed(MSG_START_SEND,
                        REMOTE_BT_ENABLE_DELAY_MS - timeElapsed);
            } else {
                // Already waited long enough for BT to come up
                // - start send.
                sendIntent();
            }
        } else {
            // Remote BT enabled already, start send immediately
            sendIntent();
        }
    }

   //发送 broadcast 交给 BT 来传输
    void sendIntent() {
        Intent intent = new Intent();
        intent.setPackage("com.android.bluetooth");
        String mimeType = MimeTypeUtil.getMimeTypeForUri(mContext, mUris.get(0));
        intent.setType(mimeType);
        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
        if (mUris.size() == 1) {
            intent.setAction(ACTION_HANDOVER_SEND);
            intent.putExtra(Intent.EXTRA_STREAM, mUris.get(0));
        } else {
            intent.setAction(ACTION_HANDOVER_SEND_MULTIPLE);
            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mUris);
        }
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        mContext.sendBroadcast(intent);
        complete();
    }    

传输的状态会由 BT 发送 broadcast 来告知 BeamStatusReceiver , 取会根据状态来更新 NFC 这边的状态 。

public class BeamStatusReceiver extends BroadcastReceiver {

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        int dataLinkType = intent.getIntExtra(EXTRA_HANDOVER_DATA_LINK_TYPE,
                BeamTransferManager.DATA_LINK_TYPE_BLUETOOTH);

        if (ACTION_CANCEL_HANDOVER_TRANSFER.equals(action)) {
            if (mTransferManager != null) {
                mTransferManager.cancel();
            }
        } else if (ACTION_TRANSFER_PROGRESS.equals(action) ||
                ACTION_TRANSFER_DONE.equals(action) ||
                ACTION_HANDOVER_STARTED.equals(action)) {
            handleTransferEvent(intent, dataLinkType);
        }
    }