文章目录

  • 前言
  • NFC 能做什么
  • NFC示例
  • 配置
  • AndroidManifest
  • 小结
  • 番外
  • 写白卡
  • 模拟卡


前言

最近在玩NFC的功能,感觉NFC的蕴含了巨大的能量,脑海中浮现各种骚操作,心情有点小激动。当然网上不乏许多优秀的文章,这里笔者只是给出自己得理解,方便快速掌握。

NFC 能做什么
  • 老人小孩 自动打电话功能
  • WIFI贴卡自动连接
  • 语音留言
  • 家长模式,监控小孩玩手机
NFC示例

这里从demo入手,站在巨人得肩膀上我们能走得更远 ,文末给出源码地址

配置

CardEmulation(卡片模拟)

  • AndroidManifest
    标准写法直接梭哈
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.android.cardemulation"
   android:versionCode="1"
   android:versionName="1.0">
   <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" />


   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
   <uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT"/>
   <uses-permission android:name="android.permission.BIND_NFC_SERVICE"/>

   <application android:allowBackup="true"
       android:label="@string/app_name"
       android:icon="@drawable/ic_launcher"
       android:theme="@style/AppTheme">

       <!-- Basic UI for sample discoverability. -->
       <activity android:name=".MainActivity"
                 android:label="@string/app_name">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />
               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>

       </activity>


       <!-- Service for handling communication with NFC terminal. -->
       <service android:name=".CardService"
                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/aid_list"/>
       </service>

   </application>
</manifest>

aid_list
作为模拟卡3000000002即为其卡号,也是建立连接得唯一标识,这里理解即可,不必深究。

  • category
    还有一个模式为payment,即付款应用,貌似是说可以自动启动,试了下没成功,此处先不管
  • android:description 自动唤起时,后台显示名称,当然还能配置图标,怎么配
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/service_name"
    android:requireDeviceUnlock="false">

    <aid-group android:description="@string/card_title" android:category="other">
        <aid-filter android:name="3000000002"/>
    </aid-group>

</host-apdu-service>

绑定服务
下面是标准代码,且必须在onResumeonPause处开关nfc

public class MainActivity extends SampleActivityBase {

    public static final String TAG = "MainActivity";
    private boolean mLogShown;
    private CardEmulation mCardEmulation;
    private ComponentName mService;
    private static List<String> AIDS = new ArrayList<>();
    private NfcAdapter mNfcAdapter;
    private PendingIntent mPendingIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        mCardEmulation = CardEmulation.getInstance(mNfcAdapter);
        mService = new ComponentName(this, CardService.class);
        AIDS.add(CardService.SAMPLE_LOYALTY_CARD_AID);
    }


    @Override
    protected void onResume() {
        super.onResume();
        mCardEmulation.setPreferredService(this, mService);
        mCardEmulation.registerAidsForService(mService, "other", AIDS);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mNfcAdapter.disableForegroundDispatch(this);
        mCardEmulation.removeAidsForService(mService, "other");
    }
}

HostApduService
开启服务,此服务主要是进行命令收发,着重讲一下其思路和流程

  • AID,即卡号需要与前面的aid-filter对应
// AID for our loyalty card service.
    public static final String SAMPLE_LOYALTY_CARD_AID = "3000000002";
  • 理解为握手标志,验证用,其实也可以不要
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
    private static final String SELECT_APDU_HEADER = "00A40400";
  • 具体传输数据标志,比如笔者这里会加入一个JSON
// Format: [Class | Instruction | Parameter 1 | Parameter 2]
    private static final String GET_DATA_APDU_HEADER = "00CA0000";
  • SELECT_OK_SW : 成功标志,加到实际数据数组尾部
  • UNKNOWN_CMD_SW : 错误标志,加到实际数据数组尾部
    因为全程为buffer传输,所以会有很多的数组切割和转换操作,这个应该不算复杂
// "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");
  • 具体交互是怎么做的呢?(processCommandApdu)
    对! 就是按照命令做一问一答
@Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        Toast.makeText(getApplicationContext(), "processCommandApdu", Toast.LENGTH_LONG);
        Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
        if (Arrays.equals(SELECT_APDU, commandApdu)) {
            return ConcatArrays("OK".getBytes(), SELECT_OK_SW);
        } else if ((Arrays.equals(GET_DATA_APDU, commandApdu))) {
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("ssid", "cameraui");
                jsonObject.put("password", "12345678");
            } catch (JSONException e) {
            }
            String account = jsonObject.toString();
            byte[] accountBytes = account.getBytes();
            Log.i(TAG, "Sending account number: " + account);
            return ConcatArrays(accountBytes, SELECT_OK_SW);
        } else {
            return UNKNOWN_CMD_SW;
        }
    }

CardReader(读卡器)

AndroidManifest

标准写法,同样是梭哈

<?xml version="1.0" encoding="UTF-8"?>
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.cardreader"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- NFC Reader Mode was added in API 19. -->
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application android:allowBackup="true"
        android:label="@string/app_name"
        android:icon="@drawable/ic_launcher"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
                  android:label="@string/app_name"
                  android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
                <!--<data-->
                <!--android:host="ext"-->
                <!--android:pathPrefix="/vndcn.com:nfc"-->
                <!--android:scheme="vnd.android.nfc" />-->
            </intent-filter>
            <intent-filter>
                    <action android:name="android.nfc.action.TAG_DISCOVERED" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <data android:mimeType="*/*" />
            </intent-filter>
            <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" />
        </activity>
    </application>


</manifest>

nfc_tech_filter
标签列表,全兼容就是了

<?xml version="1.0" encoding="utf-8"?>
<!-- This file is used as part of the filter for incoming NFC TECH_DISCOVERED intents. -->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcB</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcF</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcV</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NdefFormatable</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

读卡页面
笔者自己写的读卡页面,支持命令交互和系统启动即nfc卡片启动方式,直接继承即可

/**
 * 文件名:NFCActivity
 * 描  述:
 * 作  者:05878mq
 * 时  间:2022/8/16 15:28
 */
abstract public class NFCActivity implements LoyaltyCardReader.AccountCallback {

    final static String TAG = NFCActivity.class.getSimpleName();

    //支持的标签类型
    private final int nfcFlag = NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
            | NfcAdapter.FLAG_READER_NFC_A
            | NfcAdapter.FLAG_READER_NFC_B
            | NfcAdapter.FLAG_READER_NFC_BARCODE
            | NfcAdapter.FLAG_READER_NFC_F
            | NfcAdapter.FLAG_READER_NFC_V;

    @Override
    public void onPause() {
        disableReaderMode();
        super.onPause();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(TAG, "getIntent()" +getIntent());
        disposeIntent(getIntent());
    }

    @Override
    public void onResume() {
        try {
            enableReaderMode();
        } catch (IntentFilter.MalformedMimeTypeException e) {
            e.printStackTrace();
        }
        super.onResume();
    }

    private void disableReaderMode() {
        Log.i(TAG, "Disabling reader mode");
        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
        if (nfc != null) {
            //nfc.disableReaderMode(this);
            nfc.disableForegroundDispatch(this);
        }
    }

    private void enableReaderMode() throws IntentFilter.MalformedMimeTypeException {
        Log.i(TAG, "Enabling reader mode");
        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
        if (nfc != null) {
            IntentFilter[] FILTERS = new IntentFilter[]{new IntentFilter(
                    NfcAdapter.ACTION_TECH_DISCOVERED, "*/*")};
            String[][] TECHLISTS = new String[][]{{IsoDep.class.getName()},
                    {NfcV.class.getName()}, {NfcF.class.getName()},};

            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                    new Intent(this, this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
            nfc.enableForegroundDispatch(this, pendingIntent, FILTERS, TECHLISTS);
            nfc.enableReaderMode(this, new LoyaltyCardReader(this), nfcFlag, new Bundle());
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
//        if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.getAction()) {
//            new LoyaltyCardReader(this).onTagDiscovered(intent.getParcelableExtra(NfcAdapter.EXTRA_TAG));
//        }
        disposeIntent(intent);
    }

    private void disposeIntent(Intent intent) {
        Log.d("NfcAdapter", getIntent().getAction());
        if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.getAction()) {
            String cardId = getCardId(intent);
            if (cardId != null) {
                Log.d(TAG, String.format("NFC ID:%s", cardId));
            } else {
                Log.d(TAG, "未读取到卡ID");
            }
        }

    }

    private void readNfcTag(Intent intent) {
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
            Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
                    NfcAdapter.EXTRA_NDEF_MESSAGES);
            NdefMessage msgs[] = null;
            int contentSize = 0;
            if (rawMsgs != null) {
                msgs = new NdefMessage[rawMsgs.length];
                for (int i = 0; i < rawMsgs.length; i++) {
                    msgs[i] = (NdefMessage) rawMsgs[i];
                    contentSize += msgs[i].toByteArray().length;
                }
            }
            try {
                if (msgs != null) {
                    NdefRecord record = msgs[0].getRecords()[0];

                    //byte[] payload = record.getPayload();
                    //String res = new String(payload);

                    //数据格式定义 前两字节表示编码类型
                    //
                    String res = TextRecord.parseCustom(record).getText();
                    Log.d(TAG, "content url is: " + res);
                    getNFCInfoSuccess(res);
                }
            } catch (Exception e) {
            }
        }
    }

    protected abstract void getNFCInfoSuccess(String res);


    private String getCardId(Intent intent) {
        Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        byte[] bytesId = tagFromIntent.getId();
        readNfcTag(intent);
        Ndef ndef = Ndef.get(tagFromIntent);
        return LoyaltyCardReader.ByteArrayToHexString(bytesId);
    }

}

标签的解析
TXT 数据格式定义 前两字节表示编码类型 参考下面连接

自定义格式 使用 parseCustom

/**
 * 文件名:SS
 * 描  述:
 * 作  者:05878mq
 * 时  间:2022/8/19 17:20
 */
public class TextRecord {
    private final String mText;

    private TextRecord(String text) {
        mText = text;
    }

    public String getText() {
        return mText;
    }

    public static TextRecord parse(NdefRecord ndefRecord) {
        /*
         * 1,判断数据是否为NDEF格式
         */
        // verify tnf
        //第一个判断,TNF(类型名格式,Type Name Format)必须是NdefRecord.TNF_WELL_KNOWN
        if (ndefRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
            return null;
        }
        //第二个判断,可变的长度类型必须是NdefRecord.RTD_TEXT。
        if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
            return null;
        }

        try {
            /*
             * 2,取得读到的ndef字节流,
             * 第1个字节描述了数据的状态,然后若干个字节描述文本的语言编码,最后剩余字节表示文本数据
             */
            byte[] payload = ndefRecord.getPayload();

            /*
             * 3,解析第1个字节最高位,第7位:本流的字符编码值, 若值是0是UTF8,1是UTF16
             * 注意 字符编码与语言编码不同.
             */
            String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";
            //第1个字节第6位总为0
            /*
             * 4,解析第1个字节0-5位,它存放语言编码的长度值
             * 注意 字符编码与语言编码不同.
             */
            int languageCodeLength = payload[0] & 0x3f;

            /*
             * 5,解析语言编码
             */
            String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");

            /*
             * 6,解析出文本内容
             */
            String text = new String(payload, languageCodeLength + 1,
                    payload.length - languageCodeLength - 1, textEncoding);

            /*
             * 7,返回解析结果
             */
            return new TextRecord(text);

        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
    }
    public static TextRecord parseCustom(NdefRecord ndefRecord) {
        /*
         * 1,判断数据是否为NDEF格式
         */
        // verify tnf
        //第一个判断,TNF(类型名格式,Type Name Format)必须是NdefRecord.TNF_WELL_KNOWN
        if (ndefRecord.getTnf() != NdefRecord.TNF_MIME_MEDIA) {
            return null;
        }

        //第二个判断,可变的长度类型必须是NdefRecord.RTD_TEXT。
        byte[] CustomType = "application/xxx.xxx.xxx.android.nfc".getBytes();
        if (!Arrays.equals(ndefRecord.getType(), CustomType)) {
            return null;
        }

        try {
            /*
             * 2,取得读到的ndef字节流,
             * 第1个字节描述了数据的状态,然后若干个字节描述文本的语言编码,最后剩余字节表示文本数据
             */
            byte[] payload = ndefRecord.getPayload();

            /*
             * 6,解析出文本内容
             */
            String text = new String(payload, "UTF-8");

            /*
             * 7,返回解析结果
             */
            return new TextRecord(text);

        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
    }
}

解析成功回调
getNFCInfoSuccess

小结

到此一个标准的读写流程已经完成,此时我们仍然需要手动打开自己的APP,然后进行数据交互
那么如何能不开启APP直接拿想要的json数据,并进行下一步操作,答案是直接卡刷启动, 前面代码中注意到

byte[] CustomType = "application/xxx.xxx.xxx.android.nfc".getBytes(); 此处即自定义标签,可打开指定APP并传输自定义数据。

番外
写白卡
  • 工具写入
    拿到一张nfc白卡,推荐TagWriterTagInfo,故名思意,一读一写

主要是TagWriter 可写入包名和自定义文本,当然只是辅助工具,我们最终还是要自己实现该功能

  • 代码实现
private void writeNFC(Tag tag, String content) {
        // 这里是将数据写入NFC卡中
//            NdefRecord record1 = NdefRecord.createApplicationRecord("xxx.xxx.xxx.xxx");
//            NdefRecord record2 = NdefRecord.createExternal("xxx.xxx", "nfc", content.getBytes());

//            NdefRecord record2 = NdefRecord.createExternal("xxx.xxx", "nfc", "{\"ssid\":\"cameraui\",\"password\":\"12345678\"}".getBytes());
//            NdefRecord record1 = NdefRecord.createApplicationRecord("com.guide.capp");
//            NdefRecord record2 = NdefRecord.createExternal("xxx.xxx.xxx", "nfc", "{\"ssid\":\"cameraui\",\"password\":\"12345678\"}".getBytes());

        String mimeType = "application/xxx.xxx.xxx.android.nfc";
        byte[] mimeBytes = mimeType.getBytes(Charset.forName("UTF-8"));
        byte[] dataBytes = "{\"ssid\":\"cameraui\",\"password\":\"12345678\"}".getBytes(Charset.forName("UTF-8"));
        byte[] id = new byte[0];

        NdefRecord record3 = new NdefRecord(NdefRecord.TNF_MIME_MEDIA, mimeBytes, id, dataBytes);
        NdefRecord[] nfcs = {record3};
	    //NdefRecord[] nfcs = {record1, record2};
        NdefMessage ndefMessage = new NdefMessage(nfcs);
        int size = ndefMessage.toByteArray().length;
        try {
            Ndef ndef = Ndef.get(tag);
            if (ndef != null) {
                ndef.connect();
                if (!ndef.isWritable()) {
                    return;
                }
                if (ndef.getMaxSize() < size) {
                    return;
                }
                try {
                    ndef.writeNdefMessage(ndefMessage);
                    Toast.makeText(this, "写入成功", Toast.LENGTH_LONG).show();
                } catch (FormatException e) {
                    e.printStackTrace();
                }
            } else {
                NdefFormatable format = NdefFormatable.get(tag);
                format.connect();
                format.format(ndefMessage);
                if (format.isConnected()) {
                    format.close();
                }
                Toast.makeText(this, "写入成功", Toast.LENGTH_LONG).show();
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "写入失败", Toast.LENGTH_LONG).show();
        }
    }

这样只能写入一张实体卡,虽然能直接启动APP并执行相关操作,但是无卡的情况下,直接手机模拟ic卡岂不是更加完美

模拟卡

先分享一个软件 NFC卡模拟 主要是利用他的模拟卡功能,当然你的手机需要root,所以这是一条不归路,除非定制的手机,不建议这样玩,但这与笔者推出编译Android源码的想法不谋而合,Framework一直是笔者追求,所谓任重而道远,也祝学习的路上大家都更上一层楼,后会有期咯。

https://github.com/championswimmer/NFC-host-card-emulation-Android

https://github.com/android10/nfc_android_sample

https://github.com/TracyEminem/NFCNDEF