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阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容