5.2 大數據量的傳送
大數據量的傳送,是指圖片等數據量比較大的資源,需要通過NFC啓動藍牙的匹配,通過藍牙來傳送數據。
5.2.1 讀寫流程圖
5.2.2 發送端發送藍牙請求和發送數據流程
5.2.2.1時序圖
大數據量的寫操作跟小數據量的類似,我們這裏主要關注差異的部分,我們從P2pLinkManager.doSenpProtocol()開始。前面部分的時序圖,請查看5.1.2.1小數據量寫操作的時序圖.
5.2.2.2 代碼分析
在看P2pLinkManager.doSenpProtocol()之前,我們先看看發送數據的Apk是如何設置數據的吧。
mNfcAdapter =NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); mNfcAdapter.setBeamPushUris(newUri[]{manager.getContentUri(path)},Activity); |
以上代碼是在Gallery2中設置圖片資源的代碼,將圖片的路徑封裝在Uri數組中,並調用NfcAdapter. setBeamPushUris()進行設置。這個跟小數據量的設置類似,是將數據保存在NfcActivityState. Uris變量中。P2pLinkManager將回調NfcActivityManager.getUris()獲取到該數據。我們看看代碼吧:
void prepareMessageToSend(){ …… if (mCallbackNdef != null) { try { mMessageToSend = mCallbackNdef.createMessage(); mUrisToSend =mCallbackNdef.getUris(); return; } catch (RemoteException e) { } } } } |
P2pLinkManager. prepareMessageToSend()方法相信已經不再陌生,前面也見到過,這裏就是通過Binder回調了NfcActivityManager.getUris()方法,讀取數據,並暫存在mUrisToSend變量中。
好了,經過簡單的介紹,那麼現在我們可以從P2pLinkManager. doSenpProtocol()開始了,代碼如下:
static intdoSnepProtocol(HandoverManager handoverManager, NdefMessage msg, Uri[] uris) throws IOException { SnepClient snepClient = new SnepClient();//創建新的客戶端 try { snepClient.connect();//與socket連接 } catch (IOException e) { } try { if (uris != null) {//說明有大數據量要發送 NdefMessage response = null; //封裝藍牙標誌的請求信息在NdefMessage中 NdefMessagerequest = handoverManager.createHandoverRequestMessage(); if (request != null) { SnepMessage snepResponse= snepClient.get(request);//發送藍牙請求,並讀取另外設備迴應數據保存在snepResponse中 response =snepResponse.getNdefMessage();//將SnepMessage數據轉換成NdefMessage } // else, handover not supported if (response != null) {//有相應,可以發送數據 handoverManager.doHandoverUri(uris,response);//通過藍牙發送數據 } else if(msg != null) { snepClient.put(msg); } else { return SNEP_HANDOVER_UNSUPPORTED; } } …… } |
在大數據量傳送流程圖中也說到,發送端要發送圖片之前,需要創建標準的藍牙請求信息,然後將信息封裝在Ndefmessage中,發送給接收端,當收到接收端迴應之後才能發送真正的圖片數據。
下面我們來看看標準藍牙請求數據的創建代碼如下:
HandoverManager.createHandoverRequestMessage():
public NdefMessagecreateHandoverRequestMessage() { if (mBluetoothAdapter == null) return null;//是否支持藍牙設備 return new NdefMessage(createHandoverRequestRecord(),createBluetoothOobDataRecord());//將數據封裝在NdefMessage中 } |
當然,需要設備有藍牙的支持,否則面談,接着調用兩個接口創建兩個NdefRecord,封裝在NdefMessage中。
先看看HandoverManager.createHandoverRequestRecord()方法,藍牙請求數據:
NdefRecord createHandoverRequestRecord() { NdefMessage nestedMessage = newNdefMessage(createCollisionRecord(), createBluetoothAlternateCarrierRecord(false)); byte[] nestedPayload = nestedMessage.toByteArray();
ByteBuffer payload = ByteBuffer.allocate(nestedPayload.length +1); payload.put((byte)0x12); // connection handoverv1.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); } |
接着看HandoverManager.createBluetoothOobDataRecord(),創建當前設備藍牙地址數據:
NdefRecord createBluetoothOobDataRecord() { byte[] payload = new byte[8]; payload[0] = 0; payload[1] = (byte)payload.length;
synchronized (HandoverManager.this) { if (mLocalBluetoothAddress == null) { mLocalBluetoothAddress =mBluetoothAdapter.getAddress();//獲取當前設備藍牙地址 }
byte[]addressBytes =addressToReverseBytes(mLocalBluetoothAddress); System.arraycopy(addressBytes, 0,payload, 2, 6);//地址數據拷貝 }
return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, TYPE_BT_OOB, newbyte[]{'b'},payload);//將地址封裝在NdefRecord中 } |
上面兩個方法,創建了兩個NdefRecord,一個是藍牙請求數據,另一個是當前藍牙地址數據。好了,將量數據封裝在NdefMessage中返回。我們回頭繼續看doSnepProtocol()方法。createHandoverRequestMessage()之後就到SnepClient.get(request)發送請求了:
publicSnepMessage get(NdefMessage msg) throws IOException { SnepMessenger messenger; synchronized (this) { messenger = mMessenger; }
synchronized (mTransmissionLock) { try { messenger.sendMessage(SnepMessage.getGetRequest(mAcceptableLength,msg));//發送請求 returnmessenger.getMessage();//獲取迴應 } catch (SnepException e) { throw new IOException(e); } } } |
在發送數據之前,先調用SnepMessage.getGetRequest(mAcceptableLength,msg)將NdefMessage數據轉換成SnepMessage數據,然後調用SnepMessenger.sendMessage開始發送數據,SnepMessenger中包含了Socket的端口。
發送數據之後直接調用SnepMessenger.getMessage();獲取另一設備的迴應信息。這裏有個疑問,一直不明白,發送完數據之後立刻獲取迴應,這樣是怎麼做到同步的呢?求解釋…....
到此,我們繼續回到P2pLinkManager.doSnepProtocol()中,SnepClient.get()的get請求有了迴應,迴應的信息還是封裝在SnepMessage中,接着調用SnepMessage的方法getNdefMessage()將回應的數據轉換成NdefMessage數據。
有了迴應,說明藍牙可以匹配,調用HandoverManager.doHandoverUri(uris,response),開始通過藍牙發送Uri。
// Thisstarts sending an Uri over BT public void doHandoverUri(Uri[] uris, NdefMessage m) { if (mBluetoothAdapter == null) return;
BluetoothHandoverDatadata = parse(m);//解析迴應出藍也數據 if (data != null && data.valid){ // Register a new handover transfer object getOrCreateHandoverTransfer(data.device.getAddress(), false,true);//創建藍牙轉換器 BluetoothOppHandover handover = new BluetoothOppHandover(mContext,data.device, uris, mHandoverPowerManager, data.carrierActivating); handover.start();//開始發送數據 } } |
首先需要調用HandoverManager.parse()將回應數據解析爲藍牙數據,裏面當然包含了接收設備的藍牙地址,接着創建了一個BluetoothOppHandover()實例,這樣,該實例就包含了接收設備的藍牙地址,Uris數據,然後就調用其start()開始傳送數據了。
下面就需要看看接收端是怎麼迴應藍牙請求了的。
5.2.3 接收端迴應藍牙請求流程
5.2.3.1時序圖
5.2.3.2 代碼分析
SnepServer我們這裏也不陌生了的,裏面有ConnectionThread線程讀取收到的數據,在run方法中調用SnepServer.handleRequest()處理請求數據:
static boolean handleRequest(SnepMessenger messenger, Callbackcallback) throws IOException { SnepMessage request; try { request =messenger.getMessage();//讀取收到的數據 } catch (SnepException e) { …… } if (((request.getVersion() & 0xF0)>> 4) != SnepMessage.VERSION_MAJOR){ …… } else if (request.getField() == SnepMessage.REQUEST_GET){//get請求 messenger.sendMessage(callback.doGet(request.getAcceptableLength(), request.getNdefMessage())); } else if (request.getField() == SnepMessage.REQUEST_PUT){//put請求 if (DBG) Log.d(TAG, "putting message " +request.toString()); messenger.sendMessage(callback.doPut(request.getNdefMessage())); } else { …… } return true; } |
SnepMessenger.getMessage()這裏也不陌生了,是用來讀取收到的數據的,將數據保存在SnepMessage中。
要清楚的是,我們這裏是要回應藍牙的請求,所以這裏我們滿足了條件request.getField() ==SnepMessage.REQUEST_GET,即get請求,意思是,接收到得到的數據是其他設備的請求信息,當前設備作爲接收端,需要解析其請求數據,滿足條件後,將發送迴應信息的請求端。
callback.doGet()就是去處理請求的信息,然後返回迴應的信息,通過SnepMessenger. sendMessage()迴應發送給請求端。
先來看看callback.doGet()。這個前面也見到過,Callback接口在P2pLinkManager被實現了:
finalSnepServer.Callback mDefaultSnepCallback = newSnepServer.Callback() { @Override public SnepMessage doPut(NdefMessage msg) { onReceiveComplete(msg); returnSnepMessage.getMessage(SnepMessage.RESPONSE_SUCCESS); }
@Override public SnepMessagedoGet(intacceptableLength, NdefMessage msg) {//處理請求信息 NdefMessageresponse =mHandoverManager.tryHandoverRequest(msg);//嘗試處理請求信息,並返回迴應信息。 if (response != null){ onReceiveHandover(); returnSnepMessage.getSuccessResponse(response);返回響應信息給SnepServer }else { returnSnepMessage.getMessage(SnepMessage.RESPONSE_NOT_FOUND); } } }; |
代碼簡單,我們只需要關注HandoverManager.tryHandoverRequest(),參數類型是NdefMessage:
public NdefMessagetryHandoverRequest(NdefMessage m) { NdefRecord r = m.getRecords()[0]; //判斷數據是否是藍牙請求數據 if (r.getTnf() != NdefRecord.TNF_WELL_KNOWN) return null; if (!Arrays.equals(r.getType(), NdefRecord.RTD_HANDOVER_REQUEST)) return null; BluetoothHandoverData bluetoothData = null; for (NdefRecord oob : m.getRecords()) { if (oob.getTnf() == NdefRecord.TNF_MIME_MEDIA&& Arrays.equals(oob.getType(), TYPE_BT_OOB)) { bluetoothData =parseBtOob(ByteBuffer.wrap(oob.getPayload()));//解析藍牙數據,當然包含了發送端的藍牙地址 break; } }
synchronized(HandoverManager.this) { if (!mHandoverPowerManager.isBluetoothEnabled()) { if (!mHandoverPowerManager.enableBluetooth()) {//啓用當前設備的藍牙 return null; } } // Create the initial transfer object HandoverTransfer transfer =getOrCreateHandoverTransfer(//創建藍牙轉換器 bluetoothData.device.getAddress(), true, true); transfer.updateNotification();//發送通知準備接收圖片數據,狀態欄看到了進度條 }
// BT OOB found, whitelist it for incoming OPP data whitelistOppDevice(bluetoothData.device);//將藍牙請求端設備添加到列表中
// return BT OOB record so they can perform handover return(createHandoverSelectMessage(bluetoothActivating));創建並返回響應的數據 } |
該方法的參數是NdefMessage,第一步需要判斷數據是否是藍牙請求數據。
第二步,符合標準之後,讀取出藍牙地址數據bluetoothData。
第三步,啓用當前設備的藍牙。
第四步,將獲取到的藍牙請求端的藍牙地址數據,創建藍牙轉換器
第五步,發送通知準備接收圖片數據,這時候狀態欄那裏就可以看到進圖條了。
第六步,將藍牙請求端設備添加到列表中。
第七步,創建並返回響應的數據。這裏跟請求端創建請求數據類似,裏面也包含了當前藍牙設備的地址和迴應數據。都封裝在NdefMessage中。
Ok,接收端藍牙就開始等待接收數據了。HandoverManager.tryHandoverRequest()方法,就是完成兩件事情,第一件事情就是第一到第六步,完成接收端藍牙的匹配工作,第二件事情就是第七步,創建響應信息,並返回創建的迴應信息給到doGet()方法中,在doGet()中,將NdefMessage轉換成SnepMessage,然後返回到SnepServer中的handleRequest()方法中:
messenger.sendMessage(callback.doGet(request.getAcceptableLength(), request.getNdefMessage())); |
接着調用SnepMessenger.sendMessage(SnepMessage),發送響應數據給請求端。
請求端接收到相應數據後,就開始通過匹配藍牙發送圖片等大數據量的數據了。簡單吧。
6 Tag設備讀寫流程
6.1 Tag設備讀寫流程圖
6.2 時序圖
6.2 代碼分析
在4.2中,NFC的啓動將調用NativeNfcManager.enableDiscovery(),然後將調用到JNI方法com_android_nfc_NfcManager_enableDiscovery()掃描tag及P2p設備。在該方法中,調用phLibNfc_RemoteDev_NtfRegister()方法註冊回調函數nfc_jni_Discovery_notification_callback()。當掃面到tag或P2p的時候將回調該方法。下面看看JNI鍾開始NFC設備掃描的方法com_android_nfc_NfcManager_enableDiscovery():
static voidcom_android_nfc_NfcManager_enableDiscovery(JNIEnv *e, jobject o){ NFCSTATUS ret; struct nfc_jni_native_data *nat; CONCURRENCY_LOCK(); nat= nfc_jni_get_nat(e, o);
REENTRANCE_LOCK(); ret = phLibNfc_RemoteDev_NtfRegister(&nat->registry_info,nfc_jni_Discovery_notification_callback, (void *)nat);//註冊回調函數 REENTRANCE_UNLOCK(); …… nfc_jni_start_discovery_locked(nat,false);//開始掃描 clean_and_return: CONCURRENCY_UNLOCK(); } |
我們這裏主要看到其註冊回調函數,然後就開始掃描NFC設備了,當發現有NFC設備的時候,將會回調nfc_jni_Discovery_notification_callback()方法,代碼如下:
static voidnfc_jni_Discovery_notification_callback(void *pContext, phLibNfc_RemoteDevList_t *psRemoteDevList, uint8_t uNofRemoteDev,NFCSTATUS status) {
TRACE("Notify Nfc Service"); if((remDevInfo->RemDevType ==phNfc_eNfcIP1_Initiator) || (remDevInfo->RemDevType ==phNfc_eNfcIP1_Target)) {
hLlcpHandle = remDevHandle;
e->CallVoidMethod(nat->manager,cached_NfcManager_notifyLlcpLinkActivation, tag); …… } else {
e->CallVoidMethod(nat->manager,cached_NfcManager_notifyNdefMessageListeners,tag); …… } e->DeleteLocalRef(tag); } } |
前面也介紹過了,發現NFC設備分爲兩種,一種是Tag設備,即公交卡等卡類,另一種是手機、平板等設備,完成點對點(P2p)的數據交換。在以上方法中,分別對這兩類型的NFC設備做不同的處理。
當發現P2P設備的時候,回調了java層的NativeNfcManager.notifyLlcpLinkActivation(),當發現一個新的tag的時候,回調了java層的NativeNfcManager.notifyNdefMessageListeners().這裏我們主要關注發現新的tag,消息是如何傳送的呢?
NativeNfcManager.notifyNdefMessageListeners()代碼如下:
private void notifyNdefMessageListeners(NativeNfcTag tag){ mListener.onRemoteEndpointDiscovered(tag); } |
mListener其實就是NfcService的實例,調用了其onRemoteEndpointDiscovered()方法,代碼如下:
@Override public void onRemoteEndpointDiscovered(TagEndpoint tag){ sendMessage(NfcService.MSG_NDEF_TAG,tag); } |
這裏是發送了MSG_NDEF_TAG的Handler消息到NfcServiceHandler,是NfcService的子類。在其handleMessage()方法中處理:
case MSG_NDEF_TAG: TagEndpoint tag = (TagEndpoint) msg.obj; playSound(SOUND_START); NdefMessage ndefMsg =tag.findAndReadNdef(); if(ndefMsg != null) { tag.startPresenceChecking(); dispatchTagEndpoint(tag); } else { if(tag.reconnect()) { tag.startPresenceChecking(); dispatchTagEndpoint(tag); }else { …… } } break; |
這裏的tag,其實是NativeNfcTag的實例,調用了其findAndReadNdef()方法讀取NDEF信息,在這個方法中,調用NativeNfcTag. readNdef(),讀取消息。接着調用JNI方法,doRead()從JNI中讀取消息,然後返回給NdefMessage。因爲這裏發現的是TAG設備,所以,返回了ndefMgs=null。
接下來看tag消息的傳送。
NfcServiceHandler.dispatchTagEndPoint():
private voiddispatchTagEndpoint(TagEndpoint tagEndpoint) { Tag tag = new Tag(tagEndpoint.getUid(),tagEndpoint.getTechList(), tagEndpoint.getTechExtras(), tagEndpoint.getHandle(),mNfcTagService); registerTagObject(tagEndpoint); if (!mNfcDispatcher.dispatchTag(tag)) { unregisterObject(tagEndpoint.getHandle()); playSound(SOUND_ERROR); } else { playSound(SOUND_END); } } |
這裏面將NativeNfcTag消息用於構造一個Tag,tagEndpoint.getTechList()取出該Tag設備支持的technology。mNfcDispatcher是NfcDispatcher的實例,用於向Activity派發消息的。然後調用mNfcDispatcher.dispatchTag派發Tag消息。代碼如下:
public boolean dispatchTag(Tag tag) { NdefMessage message = null; Ndef ndef = Ndef.get(tag); …… PendingIntent overrideIntent; IntentFilter[] overrideFilters; String[][] overrideTechLists;
DispatchInfo dispatch = newDispatchInfo(mContext, tag, message); synchronized (this) { overrideFilters= mOverrideFilters; overrideIntent = mOverrideIntent; overrideTechLists = mOverrideTechLists; } …… if (tryTech(dispatch, tag)) { return true; }
} |
這個方法已經不再陌生了,前面也看到過了的。因爲我們這裏發現的是Tag設備,所以將選擇調用了NfcDispatcher.tryTech()方法,代碼如下:
boolean tryTech(DispatchInfodispatch, Tag tag) { dispatch.setTechIntent();//設置Intent的Action爲ACTION_TECH_DISCOVERED String[] tagTechs = tag.getTechList();//獲取Tag設備支持的technology Arrays.sort(tagTechs);
// Standard tech dispatch path ArrayList<ResolveInfo> matches = newArrayList<ResolveInfo>(); List<ComponentInfo> registered =mTechListFilters.getComponents();//獲取Action爲ACTION_TECH_DISCOVERED的Activity列表
// Check each registered activity to see if it matches for (ComponentInfo info : registered) { // Don't allow wild card matching if (filterMatch(tagTechs, info.techs)&&//該Activity支持解析該technology isComponentEnabled(mPackageManager,info.resolveInfo)) { // Add the activity as a match if it's not already in thelist if (!matches.contains(info.resolveInfo)) { matches.add(info.resolveInfo);//滿足條件,添加到列表中 } } }
if (matches.size() == 1) {//只有一個Activity滿足條件 // Single match, launch directly ResolveInfo info = matches.get(0); dispatch.intent.setClassName(info.activityInfo.packageName,info.activityInfo.name); if(dispatch.tryStartActivity()) { if (DBG) Log.i(TAG, "matched single TECH"); return true; } dispatch.intent.setClassName((String)null, null); } else if (matches.size()> 1) {//多個Activity滿足條件 // Multiple matches, show a custom activity chooserdialog Intent intent = new Intent(mContext,TechListChooserActivity.class); intent.putExtra(Intent.EXTRA_INTENT, dispatch.intent); intent.putParcelableArrayListExtra(TechListChooserActivity.EXTRA_RESOLVE_INFOS, matches); if (dispatch.tryStartActivity(intent)){ if (DBG) Log.i(TAG, "matched multiple TECH"); returntrue; } } return false; } |
第一步,還是需要設置Intent的Action爲ACTION_TECH_DISCOVERED。
第二步,獲取該Tag設備支持的technology。
第三步,讀取系統中Action爲ACTION_TECH_DISCOVERED的Activity列表,並獲取該Apk支持解析的technology列表,這個是在apk的Xml文件中定義的:
<?xmlversion="1.0" encoding="utf-8"?> <resourcesxmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <tech-list> <tech>android.nfc.tech.NfcA</tech> </tech-list> <tech-list> <tech>android.nfc.tech.NfcB</tech> </tech-list> </resources> |
第四步,做一個匹配,將支持解析該Tag設備technology的Apk添加到matches列表中
第五步,如果只有一個Activity滿足條件,直接啓動該Activity
第六步,如果有多個Activity滿足條件,發送Intent消息供用戶選擇啓動哪裏Activity。
到此,Tag消息就已經傳送到Activity了。那麼接下來就需要在Activity中,發起對Tag設備的讀寫了。
6.3Activity中發起對Tag設備讀寫
6.3.1 三個Intent啓動Activity
當設備掃描發現有tag或P2p設備的時候,將數據封裝好後發送Intent啓動Activity處理數據,有三類型Intent:
n ACTION_NDEF_DISCOVERED:處理NDEF數據,包含MIME、URI數據類型
n ACTION_TECH_DISCOVERED:處理非NDEF數據或者NDEF數據但不能映射爲MIME、URI數據類型
n ACTION_TAG_DISCOVERED:如果沒有Activity相應上面兩個Intent,就由該Intent處理
官網上調度圖如下:
6.3.2 Activity中對Tag設備讀寫
經過前面一大串的分析,對於Tag設備,首先需要獲取Tag信息,也說過,Tag信息包含了支持的technology類型。獲取方式如下:
Tag tagFromIntent =intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); |
我們以MifareUltralight的technology類型爲例子,根據Tag構造MifareUltralight:
MifareUltralight.get(tagFromIntent); |
看看MifareUltralight.get()接口吧:
public staticMifareUltralight get(Tag tag) { if(!tag.hasTech(TagTechnology.MIFARE_ULTRALIGHT)) returnnull;//判斷該Tag是否支持 try { return new MifareUltralight(tag); } catch (RemoteException e) { returnnull; } } |
get()方法中,需要判斷Tag設備是否支持MifareUltralight類型的technology,如果支持,那麼就真正的構造它。
得到了MifareUltralight實例後,就可以開始完成讀寫操作了。
public void writeTag(Tagtag, String tagText) { MifareUltralight ultralight = MifareUltralight.get(tag); try { ultralight.connect(); ultralight.writePage(4,"abcd".getBytes(Charset.forName("US-ASCII"))); ultralight.writePage(5,"efgh".getBytes(Charset.forName("US-ASCII"))); ultralight.writePage(6,"ijkl".getBytes(Charset.forName("US-ASCII"))); ultralight.writePage(7,"mnop".getBytes(Charset.forName("US-ASCII"))); } catch (IOException e) { Log.e(TAG, "IOException while closing MifareUltralight...",e); } finally { try { ultralight.close(); } catch (IOException e) { Log.e(TAG, "IOException while closing MifareUltralight...",e); } } }
public String readTag(Tag tag) { MifareUltralight mifare = MifareUltralight.get(tag); try { mifare.connect(); byte[] payload = mifare.readPages(4); return new String(payload, Charset.forName("US-ASCII")); } catch (IOException e) { Log.e(TAG, "IOException while writing MifareUltralight message...", e); } finally { if (mifare != null) { try { mifare.close(); } catch (IOException e) { Log.e(TAG,"Error closing tag...", e); } } } return null; }
|
不管是read還是write,都會調用到TagService中,然後是NativeNfcTag,最終到JNI中。詳細代碼就不分析了,感興趣就自己看。
參考:
http://www.eoeandroid.com/thread-173822-1-1.html
http://www.cnblogs.com/doandroid/archive/2011/11/29/2267404.html
http://blog.csdn.net/wchinaw/article/details/6542831
http://leave001.blog.163.com/blog/static/16269129320122283648327/
http://wiki.eoeandroid.com/index.php?title=NFC_Basics&diff=4589&oldid=4580
http://doandroid.info/android中nfc功能解析及代碼演示/
P2p設備http://blog.csdn.net/karen2lotus/article/details/7922948
對NFC理解很好:http://www.360doc.com/content/11/0524/13/474846_119019554.shtml