首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》近场通信

关灯直达底部

近场通信(Near Field Communication,NFC)是一种短距离(最多20cm)、高频率的无线通信技术。它是一个标准,通过把智能卡和阅读器接口集成到单个设备中,扩展了无线电频率识别(RFID)标准。该标准最初是为了在手机上使用,因此吸引了很多对非接触式数据传输感兴趣的供应商(例如信用卡销售商)。该标准使得NFC有以下3种具体应用方式:

智能卡仿真

设备是非接触式智能卡(因此其他阅读器可以读取)。

读卡器模式

设备可以读取RFID标签。

P2P模式

两个设备可以来回沟通并交换数据。

在Android 2.3(API level 9)中,Google引入了NFC功能中的读卡器模式。从Android 2.3.3(API level 10)开始,还可以将数据写到NFC标签并通过P2P模式交换。

NFC标签中包含的是使用NFC数据交换格式编码的数据,其消息格式遵循的协议是:NFC Forum Type 2 Specification。每条NDEF消息包含一条或多条NDEF记录。关于NFC的官方技术说明书可以在http://www.nfc-forum.org/获取。为了开发和测试NFC应用,强烈建议获取NFC兼容的设备(例如Nexus S,在http://www.google.com/phone/detail/nexus-s)和NFC兼容的标签。

为了在应用中使用NFC功能,需要在清单文件中声明以下许可权限:


<uses-permission android:name="android.permission.NFC" />  

为了把应用限制为使用NFC的设备,需要在manifest文件中添加以下代码:


<uses-feature android:name="android.hardware.nfc" />  

读标签

当扫描RFID/NFC标签时,读卡器模式会接收通知。在Android 2.3(API level 9)中,实现这一点的唯一方式是创建一个Activity,它监听android.nfc.action.TAG_DISCOVERED intent,当读取一个标签时会广播该intent。Android 2.3.3(API level 10)提供了更全面的方式来接收该通知,其遵循如图17-2所示的过程。

图17-2:Android 2.3.3(API level 10)中的NFC标记流

在Android 2.3.3(API level 10)以及更新的版本中,当发现NFC标签时,在Intent中会放置一个标签对象(Parcelable)作为EXTRA_TAG。然后,系统开始跟从逻辑流,确定要发送Intent的最佳Activity。在设计上,它优先把标签分发给正确的活动,而不需要向用户弹出活动选择器对话框(即在透明模式下),以避免在标签和设备之间的连接被不必要的用户交互干扰。首先要检查的是在前端是否存在Activity,其调用了enableForegroundDispatch方法。如果该Activity存在,intent就会传递给该Activity并结束;如果不存在,系统会检查标签数据的第一条NdefMessage。如果NdefRecord是URI、Smart Poster或MIME数据,系统会检查注册了ACTION_NDEF_DISCOVEREDintent并包含这种类型数据的Activity(android.nfc.action.NDEF_DISCOVERED)。如果存在该Activity,该匹配的Activity(匹配越接近越好)会接收intent。如果不存在匹配的Activity,系统会查找注册了ACTION_TECH_DISCOVERED的活动,并且匹配特定的标签技术集(再次强调,匹配越接近越好)。如果存在匹配的活动,intent会传递给该活动。然而,如果在前面的检查中都没有找到匹配的活动,intent会最终作为ACTION_TAG_DISCOVERED传递,这和Android 2.3(API level 9)处理标签的方式类似。

为了把前端的Activity设置为第一个接收标签的Activity,必须检索NFC设备适配器,并调用Activity的context引用的enableForegroundDispatch方法。实际的NFC设备适配器是由类NfcAdapter表示的。为了检索该设备的适配器,启动Android 2.3(API level 9)中的getDefaultAdapter方法或Android 2.3.3(API level 10)中的getDefaultAdapter(context)方法:


NfcAdapter adapter = NfcAdapter.getDefaultAdapter;// --- for API 10 only// NfcAdapter adapter = NfcAdapter.getDefaultAdapter(context);if(adapter != null) {        // true if enabled, false if not    boolean enabled = adapter.isEnabled;}  

一旦检索到NFC设备适配器,构建PendingIntent对象,并把它传递给enableForegroundDispatch方法。该方法必须从主线程中调用,而且Activity必须正在前端运行(调用了onResume方法):


PendingIntent intent =        PendingIntent.getActivity(this, 0,          new Intent(this, getClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),          0);NfcAdapter.getDefaultAdapter(this).enableForegroundDispatch(this, intent,  null, null);  

当Activity离开前端(调用了onPause方法)后,必须调用disableForeground-Dispatch方法:


@Overrideprotected void onPause {    super.onPause;    if(NfcAdapter.getDefaultAdapter(this) != null)        NfcAdapter.getDefaultAdapter(this).disableForegroundDispatch(this);    }}  

如果一个Activity注册了ACTION_NDEF_DISCOVERED,那么这个Activity必须把android.nfc.action.NDEF_DISCOVERED作为intent-filter,并在manifest文件中将其指定为专用的数据过滤器:


<activity android:name=".NFC233">            <!-- listen for android.nfc.action.NDEF_DISCOVERED -->            <intent-filter>                  <action android:name="android.nfc.action.NDEF_DISCOVERED"/>                  <data android:mimeType="text/*" />            </intent-filter></activity>  

这也适合TECH_DISCOVERED的情况(以下示例也包含描述特定技术的元数据资源,它保存在NFC标签中,如NDEF内容):


<activity android:name=".NFC233">            <intent-filter>                  <action android:name="android.nfc.action.TECH_DISCOVERED" />            </intent-filter>            <meta-data android:name="android.nfc.action.TECH_DISCOVERED"                android:resource="@xml/nfcfilter"        /></activity><?xml version="1.0" encoding="utf-8"?>      <!-- capture anything using NfcF or with NDEF payloads--><resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">      <tech-list>            <tech>android.nfc.tech.NfcF</tech>      </tech-list>      <tech-list>            <tech>android.nfc.tech.NfcA</tech>            <tech>android.nfc.tech.MifareClassic</tech>            <tech>android.nfc.tech.Ndef</tech>      </tech-list></resources>  

注册ACTION_TAG_DISCOVERED intent的示例manifest文件的内容如下所示:


      <!-- this will show up as a dialog when the nfc tag is scanned --><activity android:name=".NFC" android:theme="@android:style/Theme.Dialog">            <intent-filter>                  <action android:name="android.nfc.action.TAG_DISCOVERED"/>                  <category android:name="android.intent.category.DEFAULT"/>            </intent-filter></activity>  

当读取一个标签时,系统会把有效负荷作为关联数据广播intent。在Android 2.3.3(API level 10)中,Tag对象也作为EXTRA_TAG包含进来。该Tag对象提供检索特定的TagTechnology的方式,并能够执行高级操作(如I/O)。注意,该类传递和返回数组时没有使用clone方式,所以注意不要修改它们:


Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);  

在Android 2.3(API level 9)及更新的版本中,该标签的ID作为字节数组封装在intent中,其key是“android.nfc.extra.ID”(NfcAdapter.EXTRA_ID):


byte byte_id = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);  

该数据是作为Parcelable对象(NdefMessage)数组打包的,其key是“android.nfc.extra.NDEF_MESSAGES”(NfcAdapter.EXTRA_NDEF_MESSAGES):


Parcelable msgs =    intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);NdefMessage nmsgs = new NdefMessage[msgs.length];for(int i=0;i<msgs.length;i++) {    nmsgs[i] = (NdefMessage) msgs[i];}  

每个NdefMessage对象内部都有一个NdefRecord数组。该记录总是包含3比特的TNF(类型名称格式,type name format)、记录类型、唯一ID以及有效负荷。关于这方面的具体信息,可查看NdefRecord文档(http://developer.android.com/reference/android/nfc/NdefRecord.html)。现在,已经存在一些已知的类型,这里将探讨4种最常见的:TEXT、URI、SMART_POSTER和ABSOLUTE_URI:


// enum of types we are interested inprivate static enum NFCType {    UNKNOWN, TEXT, URI, SMART_POSTER, ABSOLUTE_URI}private NFCType getTagType(final NdefMessage msg) {    if(msg == null) return null;    // we are only grabbing the first recognizable item    for (NdefRecord record : msg.getRecords) {        if(record.getTnf == NdefRecord.TNF_WELL_KNOWN) {            if(Arrays.equals(record.getType, NdefRecord.RTD_TEXT)) {                return NFCType.TEXT;            }            if(Arrays.equals(record.getType, NdefRecord.RTD_URI)) {                return NFCType.URI;            }            if(Arrays.equals(record.getType, NdefRecord.RTD_SMART_POSTER)) {                return NFCType.SMART_POSTER;            }        } else if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) {            return NFCType.ABSOLUTE_URI;        }    }    return null;}  

NdefRecord.RTD_TEXT类型的有效载荷,其第一个字节会定义状态,这种类型还会对有效载荷中的文本进行编码:


/* * the First Byte of the payload contains the "Status Byte Encodings" field, * per the NFC Forum "Text Record Type Definition" section 3.2.1. * * Bit_7 is the Text Encoding Field. * * if Bit_7 == 0 the the text is encoded in UTF-8 * * else if Bit_7 == 1 then the text is encoded in UTF16 * Bit_6 is currently always 0 (reserved for future use) * Bits 5 to 0 are the length of the IANA language code. */private String getText(final byte payload) {    if(payload == null) return null;    try {        String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16";        int languageCodeLength = payload[0] & 0077;        return new String(payload, languageCodeLength + 1,                        payload.length - languageCodeLength - 1, textEncoding);    } catch (Exception e) {        e.printStackTrace;    }    return null;}  

标准URI类型(NdefRecord.RTD_URI)的有效载荷,其第一个字节定义URI的前缀:


/** * NFC Forum "URI Record Type Definition" * * Conversion of prefix based on section 3.2.2 of the NFC Forum URI Record * Type Definition document. */private String convertUriPrefix(final byte prefix) {    if(prefix == (byte) 0x00) return "";    else if(prefix == (byte) 0x01) return "http://www.";    else if(prefix == (byte) 0x02) return "https://www.";    else if(prefix == (byte) 0x03) return "http://";    else if(prefix == (byte) 0x04) return "https://";    else if(prefix == (byte) 0x05) return "tel:";    else if(prefix == (byte) 0x06) return "mailto:";    else if(prefix == (byte) 0x07) return "ftp://anonymous:[email protected]";    else if(prefix == (byte) 0x08) return "ftp://ftp.";    else if(prefix == (byte) 0x09) return "ftps://";    else if(prefix == (byte) 0x0A) return "sftp://";    else if(prefix == (byte) 0x0B) return "smb://";    else if(prefix == (byte) 0x0C) return "nfs://";    else if(prefix == (byte) 0x0D) return "ftp://";    else if(prefix == (byte) 0x0E) return "dav://";    else if(prefix == (byte) 0x0F) return "news:";    else if(prefix == (byte) 0x10) return "telnet://";    else if(prefix == (byte) 0x11) return "imap:";    else if(prefix == (byte) 0x12) return "rtsp://";    else if(prefix == (byte) 0x13) return "urn:";    else if(prefix == (byte) 0x14) return "pop:";    else if(prefix == (byte) 0x15) return "sip:";    else if(prefix == (byte) 0x16) return "sips:";    else if(prefix == (byte) 0x17) return "tftp:";    else if(prefix == (byte) 0x18) return "btspp://";    else if(prefix == (byte) 0x19) return "btl2cap://";    else if(prefix == (byte) 0x1A) return "btgoep://";    else if(prefix == (byte) 0x1B) return "tcpobex://";    else if(prefix == (byte) 0x1C) return "irdaobex://";    else if(prefix == (byte) 0x1D) return "file://";    else if(prefix == (byte) 0x1E) return "urn:epc:id:";    else if(prefix == (byte) 0x1F) return "urn:epc:tag:";    else if(prefix == (byte) 0x20) return "urn:epc:pat:";    else if(prefix == (byte) 0x21) return "urn:epc:raw:";    else if(prefix == (byte) 0x22) return "urn:epc:";    else if(prefix == (byte) 0x23) return "urn:nfc:";    return null;}  

在绝对URI(NdefRecord.TNF_ABSOLUTE_URI)类型中,整个有效负荷是以UTF-8编码并组成URI:


if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) {    String uri = new String(record.getPayload, Charset.forName("UTF-8");}  

特殊的Smart Poster(NdefRecord.RTD_SMART_POSTER)类型包含多条文本子记录或URI(或绝对URI)数据:


private void getTagData(final NdefMessage msg) {    if(Arrays.equals(record.getType, NdefRecord.RTD_SMART_POSTER)) {        try {                // break out the subrecords            NdefMessage subrecords = new NdefMessage(record.getPayload);                // get the subrecords            String fulldata = getSubRecordData(subrecords);            System.out.println("SmartPoster: "+fulldata);        } catch (Exception e) {            e.printStackTrace;        }    }}// method to get subrecord dataprivate String getSubRecordData(final NdefRecord records) {    if(records == null || records.length < 1) return null;    String data = "";    for(NdefRecord record : records) {        if(record.getTnf == NdefRecord.TNF_WELL_KNOWN) {            if(Arrays.equals(record.getType, NdefRecord.RTD_TEXT)) {                data += getText(record.getPayload) + "/n";            }            if(Arrays.equals(record.getType, NdefRecord.RTD_URI)) {                data += getURI(record.getPayload) + "/n";            } else {                data += "OTHER KNOWN DATA/n";                }            } else if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) {                data += getAbsoluteURI(record.getPayload) + "/n";            } else data += "OTHER UNKNOWN DATA/n";        }        return data;    }  

写入Tag对象

Android 2.3.3(API level 10)提供了把数据写入Tag对象的功能。为了实现此功能,必须使用Tag对象获取该标签内合适的TagTechnology。NFC标签基于很多独立的技术,提供了很多功能。TagTechnology实现了基于这些技术的各种功能。因此,需要使用NDEF技术来检索和修改标签中的NdefRecords和NdefMessages:


// get the tag from the IntentTag mytag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);// get the Ndef (TagTechnology) from the tagNdef ndefref = Ndef.get(mytag);  

注意,当使用TagTechnology执行I/O操作时需要注意如下几点:

·在使用其他I/O操作前必须调用connect方法。

·I/O操作可能是阻塞式的,而且在应用的main线程中永远都不应该调用它。

·一次只能连接一个TagTechnology。后续的connect调用会返回IOException。

·在通过TagTechnology完成I/O操作后,必须调用close方法,该方法会通过IOException撤销所有在其他线程(包括connect方法)中执行的阻塞式的I/O操作。

因此,要把数据写到tag中,会在独立于main线程的线程中调用connect方法。一旦完成该操作,会检查isConnected方法,来验证连接是否已经建立。如果连接已经建立,则会调用包含构造的NdefMessage(至少包含一条记录)的writeNdefMessage方法。写入数据之后,接下来会调用close方法来清理进程。

使用NDEF TagTechnology引用,把文本记录写入到tag中的完整代码如下:


// pass in the Ndef TagTechnology reference and the text we wish to encodeprivate void writeTag(final Ndef ndefref, final String text) {    if(ndefref == null || text == null || !ndefref.isWritable) {        return;    }    (new Thread {        public void run {            try {                Message.obtain(mgsToaster, 0,                    "Tag writing attempt started").sendToTarget;                int count = 0;                if(!ndefref.isConnected) {                    ndefref.connect;                }                while(!ndefref.isConnected) {                    if(count > 6000) {                        throw new Exception("Unable to connect to tag");                    }                    count++;                    sleep(10);                }                ndefref.writeNdefMessage(msg);                Message.obtain(mgsToaster, 0,                    "Tag write successful!").sendToTarget;            } catch (Exception t) {                t.printStackTrace;                Message.obtain(mgsToaster, 0,                    "Tag writing failed! - "+t.getMessage).sendToTarget;            } finally {                // ignore close failure...                try { ndefref.close; }                catch (IOException e) { }            }        }    }).start;}// create a new NdefRecordprivate NdefRecord newTextRecord(String text) {    byte langBytes = Locale.ENGLISH.                            getLanguage.                            getBytes(Charset.forName("US-ASCII"));    byte textBytes = text.getBytes(Charset.forName("UTF-8"));    char status = (char) (langBytes.length);    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);    return new NdefRecord(NdefRecord.TNF_WELL_KNOWN,                            NdefRecord.RTD_TEXT,                            new byte[0],                            data);}  

P2P模式和Beam

在Android 2.3.3(API level 10)中,当一个设备被设置为通过NFC把数据传输到另一个NFC数据接收设备上时,会启用P2P模式。发送数据的设备可能也会接收到数据接收设备所发送的数据,因此会出现对等(P2P)通信。在API 10中,这是通过前置推送方式来完成的,但是该方法在后期的API发布(API 14+,Android 4.0+)中被废弃了,取而代之的是一个新的推送API,称为Beam。在这一节中,我们将描述这两个方法。

API 10-13

在API 10中,NfcAdapter类的enableForegroundNdePush方法会完成P2P NFC消息交换。因此,当Activity处于活动状态(在前台运行)时,会向另一台支持com.android.npp NDEF推送协议的设备发送一条NdefMessage消息。enableForegroundNdefPush方法必须在主线程中、在通信开始之前调用(正如其onResume方法),当Activity在后台运行(在onPause方法)时,应该取消该方法。


@Overridepublic void onResume {    super.onResume;    NdefRecord rec = new NdefRecord[1];    rec[0] = newTextRecord("NFC Foreground Push Message");    NdefMessage msg = new NdefMessage(rec);    NfcAdapter.getDefaultAdapter(this).enableForegroundNdefPush(this, msg);}// create a new NdefRecordprivate NdefRecord newTextRecord(String text) {    byte langBytes = Locale.ENGLISH.                            getLanguage.                            getBytes(Charset.forName("US-ASCII"));    byte textBytes = text.getBytes(Charset.forName("UTF-8"));    char status = (char) (langBytes.length);    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);    return new NdefRecord(NdefRecord.TNF_WELL_KNOWN,                                NdefRecord.RTD_TEXT,                                new byte[0],                                data);} 

当enableForegroundNdefPush方法处在活跃状态时,标准的tag分发方式会被禁用。只有前端的活动可能会通过enableForegroundDispatch方法接收到标签发现(tag-discovered)请求。通过这种方式,可以确保其他活动和服务不会拦截正在扫描的NFC标签,从而保证正在运行的活动接收到数据。

当Activity不再在前端(调用完onPause方法后)时,注意必须调用disableFore-groundNdefPush方法:


@Overrideprotected void onPause {    super.onPause;    if(NfcAdapter.getDefaultAdapter(this) != null) {        NfcAdapter.getDefaultAdapter(this).disableForegroundNdefPush(this);    }      }  

Beam:API 14+

要使用Android Beam,其设备必须取消锁定屏幕,初始化该beam的设备必须正在运行相应的Activity。

在API 14以及更新版本中,Android Beam功能是通过NfcAdapter的两个方法来实现的:setNdefPushMessage和setNdefPushMessageCallback。setNdefPushMessage方法接收参数NdefMessage,并且会立即发送一条消息,而setNdefPushMessageCallback是异步执行的,并提供了接口NfcAdapter.CreateNdefMessageCallback。当一台设备在另一台设备的NFC通信范围内,会调用该接口的CreateNdefMessage方法。如果调用两个pushMessage方法,会先执行setNdefPushMessageCallback方法。


// here we use the callback to push a message via the NfcAdapterNfcAdapter nfcadapter = NfcAdapter.getDefaultAdapter(this);// here a callback is generatedCreateNdefMessageCallback nfccallback = new CreateNdefMessageCallback {    @Override    public NdefMessage createNdefMessage(NfcEvent event) {        String text = "Beaming via callback";        byte mimeBytes =  "application/com.oreilly.demo.android.pa.sensordemo".        getBytes(Charset.forName("US-ASCII"));NdefRecord mimeRecord = new Ndef        Record(NdefRecord.TNF_MIME_MEDIA, mimeBytes, new byte[0], text.getBytes);        NdefMessage msg = new NdefMessage(new NdefRecord {mimeRecord});        return msg;    }};nfcadapter.setNdefPushMessageCallback(nfccallback, this);// here we just push a message directlyString directtext = "Beaming Directly";byte directMimeBytes =  "application/com.oreilly.demo.android.pa.sensordemo".getBytes(Charset.forName("US-ASCII"));NdefRecord directMimeRecord = new NdefRecord(NdefRecord.TNF_MIME_MEDIA, directMimeBytes, new byte[0], directtext.getBytes);NdefMessage directmsg = new NdefMessage(new NdefRecord {directMimeRecord});nfcadapter.setNdefPushMessage(directmsg, this);  

在前面的例子中,没有使用Android Application Record(AAR),因此在manifest文件中,关于该活动的定义会包含如下intent过滤器:


<activity android:name=".NFC40">    <intent-filter>        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>        <category android:name="android.intent.category.DEFAULT"/>        <data android:mimeType="application/com.oreilly.demo.android.pa.sensordemo"/>    </intent-filter></activity>  

如果你想在应用层处理NFC操作,强烈建议你使用AAR,而不是intent过滤器(由于包名约束,AAR在活动级别不可用),因此其他应用不能干预特定的NFC操作的处理。


// generate the NdefMessage with an AARNdefMessage msg = new NdefMessage(new NdefRecord {mimeRecord,        NdefRecord.createApplicationRecord("com.oreilly.demo.android.pa.sensordemo")});nfcadapter.setNdefPushMessage(msg, this);