4、手机端开发(Android):HCE模式实现、Tag读写API调用、Intent过滤配置
好,咱们进入手机端开发环节。说实话,很多做嵌入式出身的朋友一听到「Android开发」就头大,觉得那是纯软件工程师的活。但我个人觉得,搞物联网的,手机端这块你至少得能看懂、能改、能调通。尤其是NFC配网这种场景,手机端是用户直接接触的界面,搞不好体验就全砸了。
这一章我们重点讲三个东西:HCE模式、Tag读写API、Intent过滤配置。说白了,就是手机怎么模拟成一张卡、怎么读写实体标签、以及怎么让系统正确地把NFC事件交到你的App手里。
4.1 HCE模式:让手机变成一张「虚拟卡」
HCE,全称Host-based Card Emulation,基于主机的卡模拟。嗯,这个名字有点绕,你只要记住:它让手机能像一张NFC卡片一样被读卡器读取。
我在做第一个NFC配网项目时,一开始用的是传统的SE(安全元件)方案,结果发现不同手机兼容性差得要命。后来换成HCE,清爽多了——所有数据都在App进程里处理,不需要依赖硬件安全芯片。
4.1.1 HCE的工作原理
简单说,流程是这样的:
- 读卡器(比如你的Wi-Fi模组)发出轮询请求
- 手机NFC控制器收到请求,通知系统服务
- 系统根据AID(应用ID)匹配到你的App
- App的HCE服务被唤醒,处理APDU指令
- 返回响应数据给读卡器
整个过程对用户来说是无感的。你只需要把手机往设备上一贴,数据就交换完了。
4.1.2 实现一个HCE服务
在Android里实现HCE,核心是继承HostApduService这个类。来看代码:
public class MyHceService extends HostApduService {
private static final String TAG = "MyHceService";
// 我们自定义的AID,用于配网场景
private static final String SAMPLE_AID = "F222222222";
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.d(TAG, "收到APDU指令: " + bytesToHex(commandApdu));
// 解析指令,这里简化处理
// 实际项目中需要根据ISO 7816-4协议解析
if (isSelectAidCommand(commandApdu)) {
// 选择应用指令,返回成功
return new byte[]{(byte) 0x90, (byte) 0x00};
}
// 处理自定义的配网数据请求
// 比如读取Wi-Fi SSID和密码
String wifiSsid = "MyHomeWiFi";
String wifiPwd = "password123";
String responseData = wifiSsid + "|" + wifiPwd;
return concatApduResponse(responseData.getBytes());
}
@Override
public void onDeactivated(int reason) {
Log.d(TAG, "HCE服务被停用,原因: " + reason);
// 这里可以做一些清理工作
}
// 辅助方法:判断是否是SELECT AID指令
private boolean isSelectAidCommand(byte[] apdu) {
// 实际判断逻辑略
return true;
}
// 辅助方法:拼接APDU响应(加上状态字)
private byte[] concatApduResponse(byte[] data) {
byte[] response = new byte[data.length + 2];
System.arraycopy(data, 0, response, 0, data.length);
response[data.length] = (byte) 0x90; // SW1
response[data.length + 1] = (byte) 0x00; // SW2
return response;
}
}
4.1.3 注册HCE服务
光写代码不够,还得在AndroidManifest.xml里注册服务,并且声明AID过滤规则:
<service
android:name=".MyHceService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
然后在res/xml/apduservice.xml里定义AID:
<host-apdu-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/service_description"
android:requireDeviceUnlock="false">
<aid-group
android:description="@string/aid_group_description"
android:category="other">
<aid-filter android:name="F222222222"/>
</aid-group>
</host-apdu-service>
4.2 Tag读写API:和实体NFC标签打交道
HCE是让手机模拟成卡,但很多时候我们需要手机去读实体标签——比如把配网信息预先写入一个NFC贴纸,用户一碰就完成配置。这时候就要用到Tag读写API了。
4.2.1 发现Tag并获取句柄
当手机靠近NFC标签时,系统会通过Intent把Tag对象传给你。获取方式很简单:
// 在Activity或Fragment中
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag != null) {
// 获取标签ID
byte[] tagId = tag.getId();
Log.d(TAG, "标签ID: " + bytesToHex(tagId));
// 获取支持的技术列表
String[] techList = tag.getTechList();
for (String tech : techList) {
Log.d(TAG, "支持技术: " + tech);
}
}
4.2.2 读写NDEF数据
对于配网场景,最常用的是NDEF格式的数据。因为Android原生支持NDEF解析,而且大多数NFC标签出厂就支持这个格式。
// 读取NDEF消息
private String readNdefMessage(Tag tag) {
Ndef ndef = Ndef.get(tag);
if (ndef == null) {
return null;
}
try {
ndef.connect();
NdefMessage message = ndef.getNdefMessage();
if (message != null) {
NdefRecord[] records = message.getRecords();
for (NdefRecord record : records) {
// 这里假设记录是文本格式
if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN
&& Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)) {
return new String(record.getPayload(), "UTF-8");
}
}
}
} catch (IOException | FormatException e) {
e.printStackTrace();
} finally {
try {
ndef.close();
} catch (IOException e) {
// 忽略
}
}
return null;
}
ndef.connect()总是超时。后来排查发现,这些标签的ANTICIPATION时间设置得太短,手机还没来得及建立连接就断开了。解决方案是在connect之前加一个短暂延时,或者使用NdefFormatable接口先格式化标签。
4.2.3 写入配网信息到标签
写入操作稍微复杂一点,因为需要先检查标签是否可写、是否已格式化:
private boolean writeNdefMessage(Tag tag, String data) {
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
// 标签已格式化为NDEF
try {
ndef.connect();
if (!ndef.isWritable()) {
Log.e(TAG, "标签不可写");
return false;
}
// 检查剩余空间
int size = data.getBytes().length + 10; // 加上NDEF记录开销
if (ndef.getMaxSize() < size) {
Log.e(TAG, "标签空间不足");
return false;
}
NdefRecord record = NdefRecord.createTextRecord("en", data);
NdefMessage message = new NdefMessage(new NdefRecord[]{record});
ndef.writeNdefMessage(message);
return true;
} catch (IOException | FormatException e) {
e.printStackTrace();
return false;
} finally {
try {
ndef.close();
} catch (IOException e) {
// 忽略
}
}
} else {
// 标签未格式化,尝试格式化并写入
NdefFormatable formatable = NdefFormatable.get(tag);
if (formatable != null) {
try {
formatable.connect();
NdefRecord record = NdefRecord.createTextRecord("en", data);
NdefMessage message = new NdefMessage(new NdefRecord[]{record});
formatable.format(message);
return true;
} catch (IOException | FormatException e) {
e.printStackTrace();
return false;
} finally {
try {
formatable.close();
} catch (IOException e) {
// 忽略
}
}
}
}
return false;
}
4.3 Intent过滤配置:让系统把NFC事件交给你
最后一个关键点:怎么让Android系统知道,当NFC标签被检测到时,应该启动你的App?
答案是Intent过滤。说白了,就是在AndroidManifest.xml里告诉系统:嘿,这种类型的NFC标签归我管。
4.3.1 三种调度模式
Android NFC的Intent调度有三种模式,我整理了一个表格方便对比:
| 模式 | 优先级 | 适用场景 | 配置方式 |
|---|---|---|---|
| 前台调度 | 最高 | App在前台时优先处理 | 代码中设置enableForegroundDispatch |
| Intent过滤 | 中等 | 特定类型的标签 | Manifest中声明 |
| 默认调度 | 最低 | 系统默认处理 | 无需配置 |
我个人习惯是:前台调度 + Intent过滤 组合使用。App在前台时用前台调度保证响应速度,App在后台时靠Intent过滤唤醒。
4.3.2 配置Intent过滤
在AndroidManifest.xml的Activity声明里加上:
<activity android:name=".NfcScanActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="vnd.android.nfc"
android:host="ext"
android:pathPrefix="/android.com:p2p"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter"/>
</activity>
然后在res/xml/nfc_tech_filter.xml里指定要过滤的技术类型:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<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>
tech-list里只保留对应的技术类型。比如只处理NXP的Mifare系列,就只写MifareClassic和MifareUltralight。这样可以避免你的App被其他类型的NFC标签误唤醒。
4.3.3 前台调度实现
前台调度需要在Activity的onResume和onPause中设置:
public class NfcScanActivity extends AppCompatActivity {
private NfcAdapter nfcAdapter;
private PendingIntent pendingIntent;
private IntentFilter[] intentFilters;
private String[][] techLists;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nfc_scan);
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (nfcAdapter == null) {
// 设备不支持NFC
Toast.makeText(this, "设备不支持NFC", Toast.LENGTH_LONG).show();
finish();
return;
}
// 创建前台调度的Intent
pendingIntent = PendingIntent.getActivity(
this, 0,
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE
);
// 设置Intent过滤器
IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
ndefFilter.addDataType("*/*");
} catch (IntentFilter.MalformedMimeTypeException e) {
e.printStackTrace();
}
intentFilters = new IntentFilter[]{ndefFilter};
// 设置技术列表
techLists = new String[][]{
new String[]{Ndef.class.getName()},
new String[]{NdefFormatable.class.getName()}
};
}
@Override
protected void onResume() {
super.onResume();
if (nfcAdapter != null) {
nfcAdapter.enableForegroundDispatch(
this, pendingIntent, intentFilters, techLists
);
}
}
@Override
protected void onPause() {
super.onPause();
if (nfcAdapter != null) {
nfcAdapter.disableForegroundDispatch(this);
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// 处理NFC事件
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
if (tag != null) {
// 在这里处理标签数据
handleTag(tag);
}
}
}
private void handleTag(Tag tag) {
// 具体的标签处理逻辑
String data = readNdefMessage(tag);
if (data != null) {
// 解析配网信息并执行连接
Log.d(TAG, "读取到配网数据: " + data);
}
}
}
onPause中禁用,否则你的App退出后,其他App将无法接收NFC事件。我见过有开发者忘记调用disableForegroundDispatch,结果用户每次碰标签都只能打开他的App,被用户骂惨了。
好了,手机端开发的三个核心点就讲到这里。HCE模式让手机能「装成」一张卡,Tag读写API让手机能读写实体标签,Intent过滤配置则确保系统能正确地把NFC事件路由到你的App。这三块搞定了,手机端的NFC配网基础就算打牢了。
下一章我们会讲设备端怎么配合手机完成整个配网流程,包括Wi-Fi模组的NFC驱动适配和配网协议设计。到时候见。