Android NFC 近場通訊開發技術全解

前言:


由于開發的業務需求,涉及到了Android NFC開發,所以開始了NFC開發的研究探討。寫下這博客以分享經驗,“share your knowledge to the world!”.

NFC設備的普及率很低,并且Android 3.0 開始系統才支持,而且現在NFC功能的設備并不多,這樣的應用也并不多。
設備:需要一部Andriod帶NFC的設備,至少一張標簽紙 。

NFC:近場通信Near Field Communication
首先,了解NFC標簽紙的類型有分A類,B類,F類
所以你需要知道你的應用支持什么類型的標簽,設置你需要過濾的標簽。

我們創建一個NFCActivity.java ,也就是處理我們的邏輯類。

在android Minifest.xml 文件中,你需要聲明NFCActivity,使用singleTask

    <activity
    android:name=".NFCActivity"
    android:launchMode="singleTask"
    android:screenOrientation="portrait" >

重點1:聲明Action,順序相關

如果是有內容的標簽,則優先獲得android.nfc.action.NDEF_DISCOVERED 這個Action動作

   <!-- NDEF_DISCOVERED 優先級最高的Action -->
    <intent-filter>
      <action android:name="android.nfc.action.NDEF_DISCOVERED" />
      <category android:name="android.intent.category.DEFAULT" />
      <data android:mimeType="text/plain" />
    </intent-filter>

如果未獲得上面的Action,就會獲取android.nfc.action.TECH_DISCOVERED 這個Action動作

    <!-- TECH_DISCOVERED 優先級第二的Action -->
    <intent-filter>
      <action android:name="android.nfc.action.TECH_DISCOVERED" />
      <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

如果上面兩個都沒有,就是這個優先級最低的,也是沒有任何Action的情況下最終會獲取的android.nfc.action.TAG_DISCOVERED 這個Action 動作。

<!-- TAG_DISCOVERED 優先級最低的最終處理 Action -->
<intent-filter>
  <action android:name="android.nfc.action.TAG_DISCOVERED" />
  <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

最后需要聲明你需要過濾的標簽類型

    <!-- 技術標簽過濾的便簽卡類型 android:resource="@xml/nfc_tech_filter" -->
    
     <meta-data
       android:name="android.nfc.action.TECH_DISCOVERED"
       android:resource="@xml/nfc_tech_filter" />

@xml/nfc_tech_filter,在res下面建立一個XML文件夾,然后在文件夾下面建立
Nfc_tech_filter.xml文件,名字隨便取,但是最好的見名知意的名稱。

重點2:Nfc_tech_filter.xml 文件過濾

    <tech-list>
    <tech>android.nfc.tech.Ndef</tech>
    </tech-list>

這個是一個組,組中的Ndef是代表需要捕獲F類型標簽,即F類型的才是符合條件的
如果要同時支持F類型和A類型的標簽,則是與關系,可以寫成:

    <tech-list>
             <tech>android.nfc.tech.NdeA</tech>
             <tech>android.nfc.tech.Ndef</tech>
    </tech-list>

如果我需要支持A類型,或者是F類型,則是:

    <tech-list>
            <tech>android.nfc.tech.NdeA</tech>
    </tech-list>
    <tech-list>
            <tech>android.nfc.tech.Ndef</tech>
    </tech-list>

或者關系。

然后是聲明權限:

     <uses-permission android:name="android.permission.NFC" />
     <uses-permission android:name="android.permission.GET_TASKS" />

重點3:Intent發布系統

在minifasts.xml中聲明的配置,都會在系統內部注冊,然后系統匹配哪些應用符合Action,然后檢測到這樣的Action時候就會在桌面彈出讓用戶選擇的應用列表,這就是系統的Intent發布系統,幸運的是,我們可以在代碼中截獲系統的Intent發布系統,這樣我們就可以指定特定的Action,跳轉之我們指定的應用的頁面,這樣就不需要系統彈出應用選擇的列表了,即我們在代碼中接管了系統的Intent發布系統。

獲取PendingIntent

    pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_CANCEL_CURRENT);

獲取intentFilter,
可以添加你想要的Action和mimeType:

    IntentFilter ndef = new IntentFilter();
    ndef.addAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
    ndef.addAction(NfcAdapter.ACTION_TECH_DISCOVERED);
    try { //
    ndef.addDataType("text/plain");
    } catch (IntentFilter.MalformedMimeTypeException e) {
    throw new RuntimeException("fail", e);
    }

添加卡類型:

intentFiltersArray = new IntentFilter[]{ndef};
       techListsArray = new String[][]{new String[]{
               Ndef.class.getName(), NdefFormatable.class.getName(),
               NfcA.class.getName(), MifareClassic.class.getName(),
               MifareUltralight.class.getName(), NfcB.class.getName(),
               IsoDep.class.getName(), NfcF.class.getName(), NfcV.class.getName(),
               NfcBarcode.class.getName()}};

在onNewIntent方法中

     @Override
    protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
       }

重點4,獲取Intent前臺發布系統

/**
        * 獲取Intent TAG前臺分發系統權限
        */
       private void getForgroundDispatch() {
           if (nfcAdapter != null) {
               nfcAdapter.enableForegroundDispatch(NFCActivity.this, pendingIntent, null, null);//// 獲取前臺發布系統
               hasforgrounddispatch = true;
           }
       }
      

nfcAdapter.enableForegroundDispatch(NFCActivity.this, pendingIntent, null, null);

由于我的Demo業務需求,首次通過系統Intent發布系統在應用列表中選擇了我的應用,然后應用獲取Intent發布系統。

由于獲取Intent發布系統第三個參數為null,即不指定Action,這個時候Action將會是NfcAdapter.ACTION_TAG_DISCOVERED,優先級最低的。

詳細看系統API介紹:

public void enableForegroundDispatch (Activity activity, PendingIntent intent, IntentFilter[] filters, String[][] techLists)

  • Added in API level 10
  • Enable foreground dispatch to the given Activity.
  • This will give give priority to the foreground activity when dispatching a discovered Tag to an application.
  • If any IntentFilters are provided to this method they are used to match dispatch Intents for both the ACTION_NDEF_DISCOVERED and ACTION_TAG_DISCOVERED. Since ACTION_TECH_DISCOVERED relies on meta data outside of the IntentFilter matching for that dispatch Intent is handled by passing in the tech lists separately. Each first level entry in the tech list represents an array of technologies that must all be present to match. If any of the first level sets match then the dispatch is routed through the given PendingIntent. In other words, the second level is ANDed together and the first level entries are ORed together.
  • If you pass null for both the filters and techLists parameters that acts a wild card and will cause the foreground activity to receive all tags via the ACTION_TAG_DISCOVERED intent.
  • This method must be called from the main thread, and only when the activity is in the foreground (resumed). Also, activities must call disableForegroundDispatch(Activity) before the completion of their onPause() callback to disable foreground dispatch after it has been enabled.
  • Requires the NFC permission.

上述大概意思是:

  • 至少api10以上的系統,即Android3.0
  • 為Activity獲取前臺發布系統, 你需要給出一些Activity去匹配Tag的屬性值
  • 你給予的任何的IntentFilter必須是匹配ACTION_NDEF_DISCOVERED 和ACTION_TAG_DISCOVERED.的,ACTION_TECH_DISCOVERED 是依賴于meta-data的列表說明,每一個tech list 代表一些技術的匹配,大概就是說匹配Tag標簽就是通過tech list的配置來的 。
  • 重要的是這句話:
    If you pass null for both the filters and techLists parameters that acts a wild card and will cause the foreground activity to receive all tags via the ACTION_TAG_DISCOVERED intent.
  • 即第三個參數 IntentFilter[] filters 你設置為null的時候,標簽就會獲取 優先級最低的ACTION_TAG_DISCOVERED這個Action動作

這個方法在主線程中調用,你必須要在onPause() 方法中取消捕獲的Intent分發系統

    nfcAdapter.disableForegroundDispatch(this);

核心是在OnResume中根據不同的Action去分發不同的邏輯,詳細看Demo簡單的邏輯處理

     @Override
    protected void onResume() {
    .......
    ....
    
    }

重點5 :擦除標簽內容

這個除標簽功能,在網上很難找到介紹方法,當初我也想過,寫入空信息就擦除了內容,然后自己試了試,沒有試對,然后繼續找資料,在GitHub找,然后找到了javaNFC項目,找到了方法,Java項目API跟AndroidAPI大同小異,方法介紹(“format NFC tag is just write a empty Record”)
呵呵呵呵呵呵....

傻眼了,當初自己思路是對的,原來是empty Record 構造錯了,心塞啊,又多花了一天時間找資料,(Google找資料才是王道,百度low了)
擦除數據的核心代碼:

    // TODO,格式化NFC標簽,即寫入空信息即可
    NdefRecord emptyrecord = new NdefRecord(NdefRecord.TNF_EMPTY, null, null, null);
    NdefRecord[] emptyArray = {emptyrecord};
    NdefMessage emptyMsg = new NdefMessage(emptyArray);
    startWriteMsg(getIntent(), emptyMsg);

下面是一些核心操作,沒有什么難點的:


    /**
     * 創建一個封裝要寫入的文本的NdefRecord對象
     *
     * @param text
     * @return
     */
    public NdefRecord createTextRecord(String text) {
        // 生成語言編碼的字節數組,中文編碼
        byte[] langBytes = Locale.US.getLanguage().getBytes(Charset.forName("US-ASCII"));
        // 將要寫入的文本以UTF_8格式進行編碼
        Charset utfEncoding = Charset.forName("UTF-8");
        // 由于已經確定文本的格式編碼為UTF_8,所以直接將payload的第1個字節的第7位設為0
        byte[] textBytes = text.getBytes(utfEncoding);
        int utfBit = 0;
        // 定義和初始化狀態字節
        char status = (char) (utfBit + langBytes.length);
        // 創建存儲payload的字節數組
        byte[] data = new byte[1 + langBytes.length + textBytes.length];
        // 設置狀態字節
        data[0] = (byte) status;
        // 設置語言編碼
        System.arraycopy(langBytes, 0, data, 1, langBytes.length);
        // 設置實際要寫入的文本
        System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
        // 根據前面設置的payload創建NdefRecord對象
        NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
        return record;
    }

    /**
     * 開始寫入信息
     *
     * @param intent
     * @param ndefMessage
     */
    private void startWriteMsg(Intent intent, NdefMessage ndefMessage) {
        final Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        try {
            Ndef ndef = Ndef.get(tag);
            if (ndef != null) {
                ndef.connect();
                if (!ndef.isWritable()) {
                    Tools.showTip(NFCActivity.this, "標簽是只讀!");
                    NFCActivity.this.onBackPressed();
                    NFCActivity.this.finish();
                    return;
                }
                int size = ndefMessage.toByteArray().length;
                if (ndef.getMaxSize() < size) {
                    Tools.showTip(NFCActivity.this, "空間不足!");
                    NFCActivity.this.onBackPressed();
                    NFCActivity.this.finish();
                    return;
                }
                ndef.writeNdefMessage(ndefMessage);
                Tools.showTip(NFCActivity.this, "寫入成功");// 寫入成功
                // TODO 寫入成功,操作UI
                return;
            } else {
                // 獲取可以格式化和向標簽寫入數據NdefFormatable對象
                NdefFormatable format = NdefFormatable.get(tag);
                // 向非NDEF格式或未格式化的標簽寫入NDEF格式數據
                if (format != null) {
                    try {
                        // 允許對標簽進行IO操作
                        format.connect();
                        format.format(ndefMessage);
                        Tools.showTip(NFCActivity.this, "寫入成功");// 寫入成功
                        // TODO 寫入成功,操作UI
                        return;
                    } catch (Exception e) {
                        Tools.showTip(NFCActivity.this, "寫入失敗");
                        // TODO 寫入失敗 操作UI
                        return;
                    }
                } else {
                    Tools.showTip(NFCActivity.this, "NFC標簽不支持NDEF格式");
                    // TODO NFC標簽不支持NDEF格式! 操作UI
                    return;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  • 到此,NFC功能完成,涉及到了空標簽如何識別,如何在觸發標簽,在應用前臺,非前臺如何觸發邏輯處理,如何修改標簽內容,如何擦除標簽內容,基本上所有的業務邏輯都囊括了,滿足了基本的開發需求,如果你涉及到更加復雜的需求開發,這個Demo也能很好的幫助你理解NFC邏輯跳轉,甚至可以直接搬過來使用,直接套上你的業務邏輯。當然,由于能力有限,可能有很多紕漏,望大神指點。
  • 比如這個單是NFC的邏輯跳轉就看上去代碼比較混亂,再加上你的業務邏輯,可能就沒有那么清晰了,如果你有更好的方法,把NFC技術邏輯分離出去,然后再嵌入你的業務邏輯,可能就更加完美了,做到技術跟業務分離的。代碼的可維護性就更高了。
    最后附上Demo下載地址http://download.csdn.net/detail/u011661372/8951595
    Android 智能家居開發群:468191212,歡迎從事智能開發的朋友加入,當然,一樣歡迎非智能家居的Android開發者,智能家居,未來的一片天地。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容