昨天公司突然有个要用到NFC 功能的项目,我一想前几年挺火的,网上肯定也有不少例子,觉得分分钟搞定的,然而……
虽然有很多的列子,但是能够真正跑起来并且成功的并不多。搜到的最多的是一个工具类,然后使用的方法和代码就不尽相同了。结果就是都跑起来没效果。于是,自己慢慢的去查找和调试,才慢慢理解了NFC的细节。因为网上很多都是说明和介绍,所以自己整理下NFC的简单使用,同时也是为了以后再写时提供一个参考吧。
一、NFC的三种模式
读卡器模式(Reader/writer mode)、仿真卡模式(Card Emulation Mode)、点对点模式(P2P mode)。
(1)读卡器模式
数据在NFC芯片中,可以简单理解成“刷标签”。本质上就是通过支持NFC的手机或其它电子设备从带有NFC芯片的标签、贴纸、名片等媒介中读写信息。通常NFC标签是不需要外部供电的。当支持NFC的外设向NFC读写数据时,它会发送某种磁场,而这个磁场会自动的向NFC标签供电。
(2)仿真卡模式
数据在支持NFC的手机或其它电子设备中,可以简单理解成“刷手机”。本质上就是将支持NFC的手机或其它电子设备当成借记卡、公交卡、门禁卡等IC卡使用。基本原理是将相应IC卡中的信息凭证封装成数据包存储在支持NFC的外设中 。
在使用时还需要一个NFC射频器(相当于刷卡器)。将手机靠近NFC射频器,手机就会接收到NFC射频器发过来的信号,在通过一系列复杂的验证后,将IC卡的相应信息传入NFC射频器,最后这些IC卡数据会传入NFC射频器连接的电脑,并进行相应的处理(如电子转帐、开门等操作)。
(3)点对点模式
该模式与蓝牙、红外差不多,用于不同NFC设备之间进行数据交换,不过这个模式已经没有有“刷”的感觉了。其有效距离一般不能超过4厘米,但传输建立速度要比红外和蓝牙技术快很多,传输速度比红外块得多,如过双方都使用Android4.2,NFC会直接利用蓝牙传输。这种技术被称为Android Beam。所以使用Android Beam传输数据的两部设备不再限于4厘米之内。
点对点模式的典型应用是两部支持NFC的手机或平板电脑实现数据的点对点传输,例如,交换图片或同步设备联系人。因此,通过NFC,多个设备如数字相机,计算机,手机之间,都可以快速连接,并交换资料或者服务。
二、NDEF,TECH,TAG 解析顺序
在使用之前,我们要先去了解下NFC 的tag分发系统
如果想让android设备感应到NFC标签,你要保证两点
1:屏幕没有锁住
2:NFC功能已经在设置中打开
当系统检测到一个NFC标签的时候,他会自动去寻找最合适的activity去处理这个intent.
他所发出的这个Intent将会有三种action:
ACTION_NDEF_DISCOVERED:当系统检测到tag中含有NDEF格式的数据时,且系统中有activity声明可以接受包含NDEF数据的Intent的时候,系统会优先发出这个action的intent。
ACTION_TECH_DISCOVERED:当没有任何一个activity声明自己可以响应ACTION_NDEF_DISCOVERED时,系统会尝试发出TECH的intent.即便你的tag中所包含的数据是NDEF的,但是如果这个数据的MIME type或URI不能和任何一个activity所声明的想吻合,系统也一样会尝试发出tech格式的intent,而不是NDEF.
ACTION_TAG_DISCOVERED:当系统发现前两个intent在系统中无人会接受的时候,就只好发这个默认的TAG类型的
3:NFC标签过滤
在activity的intent过滤xml声明中,你可以同时声明过滤这三种action.但是由之前所说,你应该知道系统在发送intent的时候是有优先级的,所以你最好清楚自己最想处理哪个。
①:过滤ACTION_TAG_DISCOVERED:
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
这个最简单,也是最后一个被尝试接受intent的选项。
②:过滤ACTION_NDEF_DISCOVERED:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain" />
</intent-filter>
其中最重要的应该算是data的mimeType类型了,这个定义的越准确,intent指向你这个activity的成功率就越高,否则系统可能不会发出你想要的NDEF intent了。下面在讲如何使用NDEF写入NFC标签的时候会多举几个类型的例子。
③:过滤ACTION_TECH_DISCOVERED:
你首先需要在你的<project-path>/res/xml下面创建一个过滤规则文件。名字任取,比如可以叫做nfc_tech_filter.xml。这个里面定义的是nfc实现的各种标准,每一个nfc卡都会符合多个不同的标准,个人理解为这些标准有些相互之间也是兼容的。你可以在检测到nfc标签后使用getTechList()方法来查看你所检测的tag到底支持哪些nfc标准。
一个nfc_tech_filter.xml中可以定义多个<tech-list>结构组。每一组代表我声明我只接受同时满足这些标准的nfc标签。比如A组表示,只有同时满足IsoDep,NfcA,NfcB,NfcF这四个标准的nfc标签的intent才能进入。A与B组之间的关系就是只要满足其中一个就可以了。换句话说,你的nfc标签技术,满足A的声明也可以,满足B的声明也可以。
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list> --------------------------------A组
<tech>android.nfc.tech.IsoDep</tech> <tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.NfcB</tech> <tech>android.nfc.tech.NfcF</tech>
</tech-list>
<tech-list>-----------------------------------------B组
<tech>android.nfc.tech.NfcV</tech> <tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
</resources>
过滤器过滤
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />-------------这个就是你的资源文件名
④:nfc标签前台分发系统
之所以把他也归类在nfc的过滤里面,主要是因为他跟解析nfc标签到不是那么的紧密,他解决的是接受哪些nfc标准的标签问题。所以更接近nfc的过滤。
什么叫nfc的前台发布系统?就是说当我们已经打开我们的应用的时候,那么通过这个前台发布系统的设置,我们可以让我们已经启动的activity拥有更高的优先级来依据我们在代码中定义的标准来过滤和处理intent,而不是让别的声明了intent filter的activity来干扰,甚至连自己声明在androidManifest中的intent filter都不会来干扰。也就是说foreground Dispatch的优先级大于intent filter。
第一种情况:当你的activity没有启动的时候,去扫描tag,那么系统中所有的intent filter都将一起参与过滤。
第二种情况:当你的actiity启动了,去扫描tag时,那么将直接使用你在foreground dispatch中代码写入的过滤标准。如果这个标准没有命中任何intent,那么系统将使用所有activity声明的intent filter xml来过滤。
三、MifareClassic和IsoDep使用场景
NFC常用的场景:1.读卡、2.写卡、3.分享内容
再来说说ISO xxxx,大家应该知道ISO是国际标准化组织,那个意思就是说为了世界的和平、为了世界的统一,需要约定一个大家共同认同的一个规矩,大家都按照这个规矩来做事就完了。
跟NFC有关的常见的ISO标准有:
标准 | 说明 |
---|---|
ISO 14443 | RFID卡标准(非接触IC卡),该标准又有很多子标准 |
ISO 7816 | 接触式IC卡标准 |
ISO 15693 | 某种射频卡标准吧,这个没查到资料 |
ISO 18092 | NFC标准 |
也就说如果我要实现一个国际通用的RFID卡,就需要满足ISO14443标准。
现在射频卡常用的解决方案:飞利浦的Mifare,索尼的Felica,中国人名银行的Pboc。
Mifare卡有很多种版本(详见http://en.wikipedia.org/wiki/MIFARE),常见的版本有MIFARE Classic 和MIFARE DESFire,他们分别按照ISO 14443-3 Type A和ISO 14443-4 Type A来实现。
Felica卡之前想通过ISO 14443 Type C认证,但是由于某种原因最后失败了,所以他搞了自己的一套标准叫JIS: X6319-4
Pboc是国内常见的支付卡,大部分城市的公交通都是基于Pboc解决方案实现的,据我个人的理解Pboc卡使用的是基于ISO7816接触式IC卡标准实现的接触或非接触式IC卡。
最后我们解释一下NFC的常见数据格式:NfcA/NfcB/NfcF/NfcV/IsoDep/Ndef,先看一个表:
Table 1. Supported tag technologies
Class | Description |
---|---|
TagTechnology | The interface that all tag technology classes must implement. |
NfcA | Provides access to NFC-A (ISO 14443-3A) properties and I/O operations. |
NfcB | Provides access to NFC-B (ISO 14443-3B) properties and I/O operations. |
NfcF | Provides access to NFC-F (JIS 6319-4) properties and I/O operations. |
NfcV | Provides access to NFC-V (ISO 15693) properties and I/O operations. |
IsoDep | Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations. |
Ndef | Provides access to NDEF data and operations on NFC tags that have been formatted as NDEF. |
NdefFormatable | Provides a format operations for tags that may be NDEF formattable. |
Table 2. Optional supported tag technologies
Class | Description |
---|---|
MifareClassic | Provides access to MIFARE Classic properties and I/O operations, if this Android device supports MIFARE. |
MifareUltralight | Provides access to MIFARE Ultralight properties and I/O operations, if this Android device supports MIFARE. |
这个表的意思也就说不同的芯片(解决方案、采用不同的标准实现的)卡中数据格式是不一样的,比如之前我们提到的MIFARE Classic数据格式就是NfcA,MIFARE DESFire数据格式是IsoDep,我们使用的二代身份证用的就是NfcB,Felica用的就是NfcF,德州仪器的VicinityCard卡用的是NfcV,而Android分享文件就是实用的Ndef格式传输数据。
Table2中其实是对table1的补充,可选的。
四、使用
直接上代码
首先,在 Manifest中添加权限:
<!--NFC 相关权限-->
<!--描述所需硬件特性-->
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.NFC" />
在需要实现NFC 的界面的intent-filter中添加过滤器,因为我们这次写的是NDEF类型的,所以加入
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
在这里,引入一个工具类,方便之后的操作
import android.app.Activity;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.MifareClassic;
import android.nfc.tech.Ndef;
import android.nfc.tech.NfcA;
import android.os.Build;
import android.os.Parcelable;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;
import android.widget.Toast;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class NfcUtils {
//nfc
public static NfcAdapter mNfcAdapter;
public static IntentFilter[] mIntentFilter = null;
public static PendingIntent mPendingIntent = null;
public static String[][] mTechList = null;
public NfcUtils(Activity activity) {
mNfcAdapter = NfcCheck(activity);
NfcInit(activity);
}
/**
* 检查NFC是否打开
*/
public static NfcAdapter NfcCheck(Activity activity) {
NfcAdapter mNfcAdapter = NfcAdapter.getDefaultAdapter(activity);
if (mNfcAdapter == null) {
Toast.makeText(activity, "设备不支持NFC功能!", Toast.LENGTH_SHORT).show();
return null;
} else {
if (!mNfcAdapter.isEnabled()) {
IsToSet(activity);
} else {
Toast.makeText(activity, "NFC功能已打开!", Toast.LENGTH_SHORT).show();
}
}
return mNfcAdapter;
}
/**
* 初始化nfc设置
*/
public static void NfcInit(Activity activity) {
Intent intent = new Intent(activity, activity.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
//做一个IntentFilter过滤你想要的action 这里过滤的是ndef
IntentFilter filter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
//如果你对action的定义有更高的要求,比如data的要求,你可以使用如下的代码来定义intentFilter
// IntentFilter filter2 = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
// try {
// filter.addDataType("*/*");
// } catch (IntentFilter.MalformedMimeTypeException e) {
// e.printStackTrace();
// }
// mIntentFilter = new IntentFilter[]{filter, filter2};
// mTechList = null;
try {
filter.addDataType("*/*");
} catch (IntentFilter.MalformedMimeTypeException e) {
e.printStackTrace();
}
mTechList = new String[][]{{MifareClassic.class.getName()},
{NfcA.class.getName()}};
//生成intentFilter
mIntentFilter = new IntentFilter[]{filter};
}
/**
* 读取NFC的数据
*/
public static String readNFCFromTag(Intent intent) throws UnsupportedEncodingException {
Parcelable[] rawArray = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawArray != null) {
NdefMessage mNdefMsg = (NdefMessage) rawArray[0];
NdefRecord mNdefRecord = mNdefMsg.getRecords()[0];
if (mNdefRecord != null) {
String readResult = new String(mNdefRecord.getPayload(), "UTF-8");
return readResult;
}
}
return "";
}
/**
* 往nfc写入数据
*/
public static void writeNFCToTag(String data, Intent intent) throws IOException, FormatException {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
Ndef ndef = Ndef.get(tag);
ndef.connect();
NdefRecord ndefRecord = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
ndefRecord = NdefRecord.createTextRecord(null, data);
}
NdefRecord[] records = {ndefRecord};
NdefMessage ndefMessage = new NdefMessage(records);
ndef.writeNdefMessage(ndefMessage);
}
/**
* 读取nfcID
*/
public static String readNFCId(Intent intent) throws UnsupportedEncodingException {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String id = ByteArrayToHexString(tag.getId());
return id;
}
/**
* 将字节数组转换为字符串
*/
private static String ByteArrayToHexString(byte[] inarray) {
int i, j, in;
String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
String out = "";
for (j = 0; j < inarray.length; ++j) {
in = (int) inarray[j] & 0xff;
i = (in >> 4) & 0x0f;
out += hex[i];
i = in & 0x0f;
out += hex[i];
}
return out;
}
private static void IsToSet(final Activity activity) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage("是否跳转到设置页面打开NFC功能");
// builder.setTitle("提示");
builder.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
goToSet(activity);
dialog.dismiss();
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.create().show();
}
private static void goToSet(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BASE) {
// 进入设置系统应用权限界面
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivity(intent);
return;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {// 运行系统在5.x环境使用
// 进入设置系统应用权限界面
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivity(intent);
return;
}
}
}
Manifest中的文件写好后,就可以直接去实现NFC功能了:
//在onResume中开启前台调度
@Override
protected void onResume() {
super.onResume();
//设定intentfilter和tech-list。如果两个都为null就代表优先接收任何形式的TAG action。也就是说系统会主动发TAG intent。
if (NfcUtils.mNfcAdapter != null) {
NfcUtils.mNfcAdapter.enableForegroundDispatch(this, NfcUtils.mPendingIntent, NfcUtils.mIntentFilter, NfcUtils.mTechList);
}
}
//在onNewIntent中处理由NFC设备传递过来的intent
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.e(TAG, "--------------NFC-------------" );
processIntent(intent);
}
// 这块的processIntent() 就是处理卡中数据的方法
public void processIntent(Intent intent) {
Parcelable[] rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage msg = (NdefMessage) rawmsgs[0];
NdefRecord[] records = msg.getRecords();
String resultStr = new String(records[0].getPayload());
// 返回的是NFC检查到卡中的数据
Log.e(TAG, "processIntent: "+resultStr );
try {
// 检测卡的id
String id = NfcUtils.readNFCId(intent);
Log.e(TAG, "processIntent--id: "+id );
// NfcUtils中获取卡中数据的方法
String result = NfcUtils.readNFCFromTag(intent);
Log.e(TAG, "processIntent--result: "+result );
// 往卡中写数据
ToastUtils.showLong(getActivity(),result);
String data = "this.is.write";
NfcUtils.writeNFCToTag(data,intent);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FormatException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
好了,记得在onPause()中做前台调度的取消
@Override
protected void onPause() {
super.onPause();
if (NfcUtils.mNfcAdapter != null) {
NfcUtils.mNfcAdapter.disableForegroundDispatch(this);
}
}
最后在销毁界面时
@Override
protected void onDestroy() {
super.onDestroy();
NfcUtils.mNfcAdapter = null;
}
这样,一个简单的NFC功能就实现了,如果要在Fragment中处理数据的话,可以写一个回调的方法。