NFC的工作模式
读卡器模式(Reader/writer mode)、仿真卡模式(Card Emulation Mode)、点对点模式(P2P mode)。
仿真卡模式:仿真卡模式就是将支持NFC的手机或其它电子设备当成借记卡、信用卡、公交卡、门禁卡等IC卡使用。基本原理是将相应IC卡中的信息(支付凭证)封装成数据包存储在支持NFC的手机中,且它是一个独立设备。
仿真卡需要root权限,需要在AndroidManifest.xml中添加android:sharedUserId=“android.uid.system”,并使用系统签名。
1.申请权限
<uses-permission android:name="android.permission.NFC" />
<!--API 9 设备可以使用近场通信(NFC)进行通信。-->
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<!--API 19 该设备支持基于主机的NFC卡仿真。-->
<uses-feature
android:name="android.hardware.nfc.hcef"
android:required="true" />
<!--API 24 该设备支持基于主机的NFC-F卡仿真。-->
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="true" />
2.HostApduService实现
processCommandApdu() 只要NFC阅读器向您的服务发送应用程序协议数据单元(APDU),就会调用此方法。APDU也在ISO / IEC 7816-4规范中定义。APDU是NFC读取器和HCE服务之间交换的应用程序级数据包。该应用程序级协议是半双工的:NFC读取器将向您发送命令APDU,它将等待您发送响应APDU,若里面操作时间较长可以先返回null,然后通过sendResponseApdu(byte[] responseApdu)发送。
onDeactivated() 卡片移走或断开连接时调用。
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
/**
* This is a sample APDU Service which demonstrates how to interface with the card emulation support
* added in Android 4.4, KitKat.
*
* <p>This sample replies to any requests sent with the string "Hello World". In real-world
* situations, you would need to modify this code to implement your desired communication
* protocol.
*
* <p>This sample will be invoked for any terminals selecting AIDs of 0xF11111111, 0xF22222222, or
* 0xF33333333. See src/main/res/xml/aid_list.xml for more details.
*
* <p class="note">Note: This is a low-level interface. Unlike the NdefMessage many developers
* are familiar with for implementing Android Beam in apps, card emulation only provides a
* byte-array based communication channel. It is left to developers to implement higher level
* protocol support as needed.
*
* <p>交互Apdu格式:[apdu header]+[dataLength]+[data]+[status]
*/
public class CardEmulationService extends HostApduService {
private static final String TAG = "CardEmulation";
// AID for our loyalty card service.
private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222";
// ISO-DEP command HEADER for selecting an AID.
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final String SELECT_APDU_HEADER = "00A40400";
private static final String UPDATE_APDU_HEADER = "00B40400";
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final String GET_DATA_APDU_HEADER = "00CA0000";
// "OK" status word sent in response to SELECT AID command (0x9000)
private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
// "UNKNOWN" status word sent in response to invalid APDU command (0x0000)
private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
private static final String WRITE_DATA_APDU_HEADER = "00DA0000";
private static final String READ_DATA_APDU_HEADER = "00EA0000";
private static String dataStr = null;
/*File IO Stuffs*/
File sdcard = Environment.getExternalStorageDirectory();
File file = new File(sdcard, "file.txt");
StringBuilder text = new StringBuilder();
int pointer;
@Override
public void onDeactivated(int reason) {
}
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
if (commandApdu.length < 6) {
return UNKNOWN_CMD_SW;
}
StringBuffer buffer = new StringBuffer();
byte[] header = new byte[4];
int pos = 0;
System.arraycopy(commandApdu, pos, header, 0, header.length);
buffer.append("header:").append(ByteArrayToHexString(header));
pos += header.length;
if (commandApdu.length == 6) {
return SELECT_OK_SW;
}
int dataLen = Integer.parseInt(ByteArrayToHexString(new byte[]{commandApdu[pos++]}));
buffer.append("\ndataLen:").append(dataLen);
if (commandApdu.length < pos + dataLen) {
return ConcatArrays(buffer.toString().getBytes(), SELECT_OK_SW);
}
byte[] data = new byte[dataLen];
System.arraycopy(commandApdu, pos, data, 0, dataLen);
buffer.append("\ndata:").append(ByteArrayToHexString(data));
return ConcatArrays(buffer.toString().getBytes(), SELECT_OK_SW);
}
/**
* Build APDU for SELECT AID command. This command indicates which service a reader is
* interested in communicating with. See ISO 7816-4.
*
* @param aid Application ID (AID) to select
* @return APDU for SELECT AID command
*/
public static byte[] BuildSelectApdu(String aid) {
// Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA]
return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", aid.length() / 2) + aid);
}
/**
* Utility method to convert a byte array to a hexadecimal string.
*
* @param bytes Bytes to convert
* @return String, containing hexadecimal representation.
*/
public static String ByteArrayToHexString(byte[] bytes) {
final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles)
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value
hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble
hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble
}
return new String(hexChars);
}
/**
* Utility method to convert a hexadecimal string to a byte string.
*
* <p>Behavior with input strings containing non-hexadecimal characters is undefined.
*
* @param s String containing hexadecimal characters to convert
* @return Byte array generated from input
* @throws java.lang.IllegalArgumentException if input length is incorrect
*/
public static byte[] HexStringToByteArray(String s) throws IllegalArgumentException {
int len = s.length();
if (len % 2 == 1) {
throw new IllegalArgumentException("Hex string must have even number of characters");
}
byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters
for (int i = 0; i < len; i += 2) {
// Convert each character into a integer (base-16), then bit-shift into place
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Utility method to concatenate two byte arrays.
*
* @param first First array
* @param rest Any remaining arrays
* @return Concatenated copy of input arrays
*/
public static byte[] ConcatArrays(byte[] first, byte[]... rest) {
int totalLength = first.length;
for (byte[] array : rest) {
totalLength += array.length;
}
byte[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (byte[] array : rest) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
private void readFromFile() {
// try {
// BufferedReader br = new BufferedReader(new FileReader(file));
// String line;
//
// while ((line = br.readLine()) != null) {
// text.append(line);
// text.append('\n');
// }
// }
// catch (IOException e) {
// e.printStackTrace();
// }
text.append("some string random data some string random data some string random data some string random data some string random data \n");
text.append("some string random data some string random data some string random data some string random data some string random data \n");
text.append("some string random data some string random data some string random data some string random data some string random data \n");
text.append("some string random data some string random data some string random data some string random data some string random data \n");
text.append("some string random data some string random data some string random data some string random data some string random data \n");
text.append("some string random data some string random data some string random data some string random data some string random data \n");
}
}
3.注册Service
<service
android:name=".CardEmulationService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<!-- Intent filter indicating that we support card emulation. -->
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<!-- Required XML configuration file, listing the AIDs that we are emulating cards
for. This defines what protocols our card emulation service supports. -->
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
4.apduservice.xml配置
apudservice.xml中配置AID,可以有一个或多个,aid是这个service标识,需要跟读卡器端约定好,通过aid来找到这个service;这里也可以不配置aid,在activity/fragment中动态注册aid。
requireDeviceUnlock: 可用于指定在调用此服务以处理APDU之前必须解锁设备。
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/apdu_service_description"
android:requireDeviceUnlock="false">
<aid-group
android:category="other"
android:description="@string/apdu_service_description">
<!-- <aid-filter android:name="F222222222" />-->
</aid-group>
</host-apdu-service>
5.动态注册AID
import android.content.ComponentName;
import android.nfc.NfcAdapter;
import android.nfc.cardemulation.CardEmulation;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.ArrayList;
import java.util.List;
public class CardEmulationFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_card_emulation, container, false);
}
private static final List<String> AIDS = new ArrayList<>();
static {
AIDS.add("F222222222");
AIDS.add("F223344556");
}
private CardEmulation mCardEmulation;
private ComponentName mService;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
NfcAdapter mNfcAdapter = NfcAdapter.getDefaultAdapter(getContext());
mCardEmulation = CardEmulation.getInstance(mNfcAdapter);
mService = new ComponentName(getActivity(), CardEmulationService.class);
}
@Override
public void onResume() {
super.onResume();
Log.d("CardEmulation", "registerAidsForService");
mCardEmulation.setPreferredService(getActivity(), mService);
mCardEmulation.registerAidsForService(mService, "other", AIDS);
}
@Override
public void onPause() {
super.onPause();
Log.d("CardEmulation", "removeAidsForService");
mCardEmulation.removeAidsForService(mService, "other");
mCardEmulation.unsetPreferredService(getActivity());
}
}
6.读卡器端Apdu处理
这里简单介绍一下读卡器端对Tag的处理,详细的请看Android NFC之读卡器模式
import android.nfc.Tag;
import android.nfc.tech.IsoDep;
import java.io.IOException;
import java.util.Arrays;
public class CustomIsoDepReader extends CardReader {
private static final String TAG = "IsoDepReader";
private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222";
// ISO-DEP command HEADER for selecting an AID.
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
private static final String SELECT_APDU_HEADER = "00A40400";
// "OK" status word sent in response to SELECT AID command (0x9000)
private static final byte[] SELECT_OK_SW = {(byte) 0x90, (byte) 0x00};
public String parse(Tag tag) {
IsoDep isoDep = IsoDep.get(tag);
try {
isoDep.connect();
byte[] command = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
byte[] result = isoDep.transceive(command);
int resultLength = result.length;
byte[] statusWord = {result[resultLength - 2], result[resultLength - 1]};
byte[] payload = Arrays.copyOf(result, resultLength - 2);
if (Arrays.equals(SELECT_OK_SW, statusWord)) {
return new String(payload, "UTF-8");
}
return "Select failed";
} catch (IOException e) {
e.printStackTrace();
return e.getMessage();
} finally {
close(isoDep);
}
}
/**
* Build APDU for SELECT AID command. This command indicates which service a reader is
* interested in communicating with. See ISO 7816-4.
*
* @param aid Application ID (AID) to select
* @return APDU for SELECT AID command
*/
private byte[] BuildSelectApdu(String aid) {
// Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA]
return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", aid.length() / 2) + aid);
}
}