Android - 學習操作NFC - 2
在<Android - 學習操作NFC – 1>說明了Android在處理NFC tag的機制、tag dispatch system的運作流程,以及三種
ACTION_NDEF_DISCOVERED、ACTION_TECH_DISCOVERED與ACTION_TAG_DISCOVERED的處理方式與intent filter註冊方法。
該篇主要針對如何處理ACTION_NDEF_DISCOVERED的Reader、Writer進行說明。
首先說明如何撰寫常用的NDEF Records:
〉Creating Common Types of NDEF Records:
上一篇介紹簡單Reader的方式,該篇先由Write tag開始。因此,如果今天應用程式使用Android 4.0(API Level 14)使用createUri()
的方法幫助自動建立一個URI records;使用Android 4.1(API level 16)則可透過createExternal()與createMime()幫助建立MIME與
external type的NDEF records。藉由使用這些方法以協助建立NDEF records。
以下便介紹操作NDEF message的第一個Record,來寫入資料至NFC tag或Beaming。
A. TNF_ABSOLUTE_URI:
建議使用RTD_URI類型取代TNF_ABSOLUTE_URI,因為RTD_URI是更有效的。
‧Write:建立一個TNF_ABSOLUTE_URI的Ndef record:
// 建立個NdefRecord,指定type與payload
NdefRecord uriRecord = new NdefRecord(
NdefRecord.TNF_ABSOLUTE_URI ,
"http://developer.android.com/index.html".getBytes(Charset.forName("US-ASCII")),
new byte[0],
new byte[0]);
‧Read:定義intent filter取得Ndef record:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http"
android:host="developer.android.com"
android:pathPrefix="/index.html" />
</intent-filter>
註冊的Intent Filter只有在tag dispatch system偵測到對應的Ndef tag時才會觸發。
B. TNF_MIME_MEDIA:
‧Write:
(1) 建立一個TNF_MIME_MEDIA Ndef record:使用createMime()方法;(但僅在Android 4.1(API Level 16)以後才支援)
// 指定MIME類型,再將內容轉成byte[]
NdefRecord mimeRecord = NdefRecord.createMime(
"application/vnd.com.example.android.beam",
"Beam me up, Android".getBytes(Charset.forName("US-ASCII")));
(2) 建立一個TNF_MIME_MEDIA:使用NdefRecord物件:
// 指定Type、MIME Type與payload
NdefRecord mimeRecord = new NdefRecord(
NdefRecord.TNF_MIME_MEDIA ,
"application/vnd.com.example.android.beam".getBytes(Charset.forName("US-ASCII")),
new byte[0],
"Beam me up, Android!".getBytes(Charset.forName("US-ASCII")));
‧Read:讀取MIME:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<!-- 定義要處理的MIME Type -->
<data android:mimeType="application/vnd.com.example.android.beam" />
</intent-filter>
C. TNF_WELL_KNOWN with RTD_TEXT:
‧Write:建立一個TNF_WELL_KNOWN的Ndef record:
public NdefRecord createTextRecord(String payload, Locale locale, boolean encodeInUtf8) {
// 取得預設的編碼格式
byte[] langBytes = locale.getLanguage().getBytes(Charset.forName("US-ASCII"));
// 準備轉換成UTF-8的編碼
Charset utfEncoding = encodeInUtf8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16");
// 將內容依預設編碼轉成byte[]
byte[] textBytes = payload.getBytes(utfEncoding);
// 往下做字元轉換的位移
int utfBit = encodeInUtf8 ? 0 : (1 << 7);
char status = (char) (utfBit + 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);
// 建立TNF_WELL_KNOWN的Ndef record
NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,
NdefRecord.RTD_TEXT, new byte[0], data);
return record;
}
‧Read:定義intent filter取得資料:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
D. TNF_WELL_KNOWN with RTD_URI:
‧Write:建立TNF_WELL_KNOWN Ndef record,內容有RTD_URI;其方式與建立RTD_URI相似有分成二個:
(1) 建立URI有二個方式,一個由String –> URI;另一個是直接用URI物件,如下:
NdefRecord rtdUriRecord1 = NdefRecord.createUri("http://example.com");
// 上下為相同效果
Uri uri = new Uri("http://example.com");
NdefRecord rtdUriRecord2 = NdefRecord.createUri(uri);
byte[] uriField = "example.com".getBytes(Charset.forName("US-ASCII"));
//add 1 for the URI Prefix
byte[] payload = new byte[uriField.length + 1];
//prefixes http://www. to the URI
byte payload[0] = 0x01;
//appends URI to payload
System.arraycopy(uriField, 0, payload, 1, uriField.length);
NdefRecord rtdUriRecord = new NdefRecord(
NdefRecord.TNF_WELL_KNOWN,
NdefRecord.RTD_URI,
new byte[0],
payload);
‧Read:定義要處理的intent filter:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http"
android:host="example.com"
android:pathPrefix="" />
</intent-filter>
E. TNF_EXTERNAL_TYPE:
‧Write:使用createExternal()方法;或使用NdefRecord的方法手動建立;
//assign to your data
byte[] payload;
//usually your app's package name
String domain = "com.example";
String type = "externalType";
// 指定domain, type, payload
NdefRecord extRecord = NdefRecord.createExternal(domain, type, payload);
byte[] payload;
...
// 注意寫入的格式為: {domain}:{type}
NdefRecord extRecord = new NdefRecord(
NdefRecord.TNF_EXTERNAL_TYPE,
"com.example:externalType",
new byte[0],
payload);
‧Read:註冊要處理的intent filter:
<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="/com.example:externalType"/>
</intent-filter>
使用TNF_EXTERNAL_TYPE是比較好用於一般的NFC tag,以支持Android或非Android系統可以讀取到這些Tag。
另外,要注意TNF_EXTERNAL_TYPE的URNs定義格式,如下:「urn:nfc:ext:example.com:externalType
」;
根據NFC Forum RTD規格宣告[urn:nfc:ext]在某些Ndef message會被省略掉,因此,需要額外定義 domain (例如:example.com)與
type (例如:externalType)。當dispatching TNF_EXTERNAL_TYPE時,Android轉換 run:nfc:ext:example.com:externalType URN為
vnd.andorid.nfc://ext/example.com:externalType的URI,所以在定義intent filter時,在scheme為:vnd.android.nfc;host為ext;
patchPrefix為/example.com:externalType。
以上介紹了幾個常用Ndef message與Ndef record type的撰寫方式,需注意的是在Read的部分,在應用程式尚未被啟動時,
如果系統偵測到有Ndef Tag,它會發出對應的Intent,讓有註冊Filter intent接收到這個intent,進一步讓用戶選擇要執行的
應用程式。因此,如果應用程式本身支持多種不同的Filter intent均要記得加上去。
往下針對程式面說明要將上述的內容怎麼發佈給device或NFC tag。需要有那些重要的類別來加以完成。
參考<http://nfc.android.com/>中的<StickyNotes sample code>範例來加以說明:
步驟0-1:建立專案,指定使用<uses-sdk />要大於等於10,並且加入必要的<uses-permission />;
<uses-sdk android:minSdkVersion="10" />
<uses-permission android:name="android.permission.NFC"></uses-permission>
步驟0-2:標記預設應用程式要處理的Intent Filter;
<application android:icon="@drawable/icon" android:label="@string/app_name">
<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>
<!-- 註冊在應用程式外,系統所廣播出來的intent filter -->
<intent-filter>
<!-- 註冊僅處理NDEF Tag,並指定預設啟動的Activity與處理的Type -->
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
註冊應用程式要處理的Intent Filter,此部分註冊的是應用程式非在前景模式時,如果系統有偵測到NDEF tag發出intent,
應用程式註冊了該intent filter則會被觸發到,如果有多個註冊相同的intent filter則會需要用戶進行選擇。
步驟1:處理應用程式註冊的Intent Filter,透過OnResume()事件進行處理將讀取的內容放置畫面的EditText中;
1-1. 畫面配置:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<LinearLayout
android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<Button
android:id="@+id/write_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Write to Tag" >
</Button>
<EditText
android:id="@+id/note"
android:layout_width="fill_parent"
android:layout_height="388dp"
android:gravity="top"
android:text="Edit me." >
</EditText>
</LinearLayout>
</RelativeLayout>
放置一個Button與EditText來顯示讀取到的內容,與負責寫入資料至NDEF Tag。
1-2. override處理OnResume()收到由系統送來的intent;
@Override
protected void onResume()
{
super.onResume();
// 處理由Android系統送出應用程式處理的intent filter內容
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
// 取得NdefMessage
NdefMessage[] messages = getNdefMessages(getIntent());
// 取得實際的內容
byte[] payload = messages[0].getRecords()[0].getPayload();
setNoteBody(new String(payload));
// 往下送出該intent給其他的處理對象
setIntent(new Intent());
}
}
1-3. 拆解從Ndef Tag中取得的原始資料,並且轉換成NdefMessage內容;
NdefMessage[] getNdefMessages(Intent intent) {
// Parse the intent
NdefMessage[] msgs = null;
String action = intent.getAction();
// 識別目前的action為何
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
// 取得parcelabelarrry的資料
Parcelable[] rawMsgs =
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
// 取出的內容如果不為null,將parcelable轉成ndefmessage
if (rawMsgs != null) {
msgs = new NdefMessage[rawMsgs.length];
for (int i = 0; i < rawMsgs.length; i++) {
msgs[i] = (NdefMessage) rawMsgs[i];
}
} else {
// Unknown tag type
byte[] empty = new byte[] {};
NdefRecord record = new NdefRecord(NdefRecord.TNF_UNKNOWN, empty, empty, empty);
NdefMessage msg = new NdefMessage(new NdefRecord[] {
record
});
msgs = new NdefMessage[] {
msg
};
}
} else {
Log.d(TAG, "Unknown intent.");
finish();
}
return msgs;
}
1-4. 設得資料內容並回寫至畫面中;
private void setNoteBody(String body) {
Editable text = gNote.getText();
text.clear();
text.append(body);
}
1-5. 讓應用程式在前景模式下也能直接處理偵測到的Ndef Tdg,讓系統偵測到Ndef Tag時無需再重新啟動相同的應用程式;
為了讓應用程式在前景也可以處理intent filter,需要建立幾個必要的項目:
(1) 宣告PendingIntent:註冊讓應用程式的Activity負責處理所有接受到的NFC intents:
// 註冊讓該Activity負責處理所有接收到的NFC Intents。
gNfcPendingIntent = PendingIntent.getActivity(
this, 0,
// 指定該Activity為應用程式中的最上層Activity
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
在onCreate()中建立該PendingIntent,並且將gNfcPendingIntent宣告成全域變數。指定負責的Activity為最上層的Activity。
(2) 宣告IntentFilter[]:註冊要在前景處理的Intent Filter類型;
// 建立要處理的Intent Filter負責處理來自Tag或p2p交換的資料。
IntentFilter ndefDetected = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
ndefDetected.addDataType("text/plain");
} catch (MalformedMimeTypeException e) { }
gNdefExchangeFilters = new IntentFilter[] { ndefDetected };
在onCreate()宣告要處理的IntentFilter,指定要處理的Data Type為MIME的文字,最後將IntentFilter加入全域變數的gNdefExchangeFilters。
(3) 覆寫onRsume()事件,讓Activity啟動時啟動NfcAdapter支持前景模式下處理NFC Intent;
@Override
protected void onResume()
{
super.onResume();
gResumed = true;
// 處理由Android系統送出應用程式處理的intent filter內容
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
// 取得NdefMessage
NdefMessage[] messages = getNdefMessages(getIntent());
// 取得實際的內容
byte[] payload = messages[0].getRecords()[0].getPayload();
setNoteBody(new String(payload));
// 往下送出該intent給其他的處理對象
setIntent(new Intent());
}
// 啟動前景模式支持Nfc intent處理
enableNdefExchangeMode();
}
/**
* 啟動Ndef交換資料模式。
*/
private void enableNdefExchangeMode() {
// 讓NfcAdapter啟動能夠在前景模式下進行intent filter的dispatch。
gNfcAdapter.enableForegroundDispatch(
this, gNfcPendingIntent, gNdefExchangeFilters, null);
}
在onResume()中加入enableNdefExchangeMode()方法,裡面使用了「gNfcAdapter.enableForegroundDispatch()」方法,
啟動NfcAdapter支持前景模式下處理NFC Intent。
(4) 覆寫onNewIntent()事件,補捉由其他應用程式或系統發出的Intent進行處理;
@Override
protected void onNewIntent(Intent intent) {
// 覆寫該Intent用於補捉如果有新的Intent進入時,可以觸發的事件任務。
// NDEF exchange mode
if (!gWriteMode && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
NdefMessage[] msgs = getNdefMessages(intent);
promptForContent(msgs[0]);
}
}
/**
* 應用程式補捉到Ndef Message,詢問用戶是否要取代目前畫面中的文件。
* @param msg
*/
private void promptForContent(final NdefMessage msg) {
new AlertDialog.Builder(this).setTitle("Replace current content?")
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
String body = new String(msg.getRecords()[0].getPayload());
setNoteBody(body);
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
}
}).show();
}
覆寫onNewIntent()事件以補捉當Activity收到系統送來的Intent時可以直接進行處理,不需要重新建立一個新的Activity處理。
直接使用已存在的instance負責,另外增加promptForContent()方法來詢問用戶如果畫面中有資料是否要清除。
完成步驟0至步驟1,即可以完成讀取Ndef tag的功能。接下來步驟2要說明的是如何寫入text/plain的內容至Ndef tag;
步驟2:要讓App可以操作NFC,需要先取得NdefAdapter物件,才能啟動寫入資料至NFC tag或是其他應用程式;
2-1. 先針對畫面中按鈕、文字框二個控制項,並且分加入對應的Listener;
@Override
protected void onCreate(Bundle savedInstanceState)
{
// ...
// 取得EditText與Button,並且註冊對應的事件
findViewById(R.id.write_tag).setOnClickListener(this.gTagWriter);
gNote = (EditText)findViewById(R.id.note);
gNote.addTextChangedListener(gTextWatcher);
// ...
}
a. Button註冊OnClickListener(),實作事件以啟動寫入資料至Tag或應用程式;
private View.OnClickListener gTagWriter = new View.OnClickListener() {
@Override
public void onClick(View v)
{
// 先停止接收任何的Intent,準備寫入資料至tag;
disableNdefExchangeMode();
// 啟動寫入Tag模式,監測是否有Tag進入
enableTagWriteMode();
// 顯示對話框,告知將Tag或手機靠近本機的NFC感應區
new AlertDialog.Builder(MainActivity.this)
.setTitle("Touch tag to write")
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog)
{
// 在取消模式下,先關閉監偵有Tag準備寫入的模式,再啟動等待資料交換的模式。
// 停止寫入Tag模式,代表已有Tag進入
disableTagWriteMode();
// 啟動資料交換
enableNdefExchangeMode();
}
}).create().show();
}
};
/**
* 啟動Ndef交換資料模式。
*/
private void enableNdefExchangeMode()
{
// 讓NfcAdatper啟動前景Push資料至Tag或應用程式。
gNfcAdapter.enableForegroundNdefPush(MainActivity.this, getNoteAsNdef());
// 讓NfcAdapter啟動能夠在前景模式下進行intent filter的dispatch。
gNfcAdapter.enableForegroundDispatch(this, gNfcPendingIntent, gNdefExchangeFilters, null);
}
private void disableNdefExchangeMode()
{
gNfcAdapter.disableForegroundNdefPush(this);
gNfcAdapter.disableForegroundDispatch(this);
}
/**
* 啟動Tag寫入模式,註冊對應的Intent Filter來前景模式監聽是否有Tag進入的訊息。
*/
private void enableTagWriteMode()
{
gWriteMode = true;
IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
gWriteTagFilters = new IntentFilter [] {tagDetected};
gNfcAdapter.enableForegroundDispatch(this, gNfcPendingIntent, gWriteTagFilters, null);
}
/**
* 停止Tag寫入模式,取消前景模式的監測。
*/
private void disableTagWriteMode()
{
gWriteMode = false;
gNfcAdapter.disableForegroundDispatch(this);
}
該按鈕主要在點擊後,註冊對應的Intent Filter:ACTION_TAG_DISCOVERED,為了等待有Nfc Tag進入監偵範圍所觸發Intent事件,
因此,搭配在onNewIntent()裡增加了寫識別intent.action進一步將EditText的內容寫入Tag中;
但在啟動另一個監偵時,記得先將預先監偵的ACTION_NDEF_DISCOVERED先取消,以免衝突。等到onCancel()事件啟動時,
再取消ACTION_TAG_DISCOVERED的監偵,重新啟動ACTION_NDEF_DISCOVERED的監聽。
=>另外需注意在enableNdefExchangedMode()裡,也啟動了前景模式發送資訊至應用程式。
b. EditText註冊TextChangedListener(),宣告一個TextWatcher,處理在afterTextChanged()下啟動寫資料至應用程式;
private TextWatcher gTextWatcher = new TextWatcher() {
@Override
public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
}
@Override
public void afterTextChanged(Editable arg0) {
// 如果是在Resume的狀態下,當編輯完後,啟動前景發佈訊息的功能。
if (gResumed) {
gNfcAdapter.enableForegroundNdefPush(MainActivity.this, getNoteAsNdef());
}
}
};
在afterTextChanged()事件裡,先識別目前是否從onResume()進入,如果是將啟動NfcAdapter在前景模式推送訊息至其他App。
2-2. 增加onNewIntent()事件,處理當監偵到Ndef Tag時,寫入資料至Tag中;
@Override
protected void onNewIntent(Intent intent)
{
// 覆寫該Intent用於補捉如果有新的Intent進入時,可以觸發的事件任務。
// NDEF exchange mode
if (!gWriteMode && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
NdefMessage [] msgs = getNdefMessages(intent);
promptForContent(msgs[0]);
}
// 監測到有指定ACTION進入,代表要寫入資料至Tag中。
// Tag writing mode
if (gWriteMode && NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
writeTag(getNoteAsNdef(), detectedTag);
}
}
2-3. 寫入Ndef資料的方法;
boolean writeTag(NdefMessage message, Tag tag) {
int size = message.toByteArray().length;
try {
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
ndef.connect();
if (!ndef.isWritable()) {
toast("Tag is read-only.");
return false;
}
if (ndef.getMaxSize() < size) {
toast("Tag capacity is " + ndef.getMaxSize() + " bytes, message is " + size
+ " bytes.");
return false;
}
ndef.writeNdefMessage(message);
toast("Wrote message to pre-formatted tag.");
return true;
} else {
NdefFormatable format = NdefFormatable.get(tag);
if (format != null) {
try {
format.connect();
format.format(message);
toast("Formatted tag and wrote message");
return true;
} catch (IOException e) {
toast("Failed to format tag.");
return false;
}
} else {
toast("Tag doesn't support NDEF.");
return false;
}
}
} catch (Exception e) {
toast("Failed to write tag");
}
return false;
}
private void toast(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
writeTag()方法增加了一些判斷的邏輯很值得參考。
2-4. 覆寫onPasue()事件,讓Activity暫停時關閉NfcAdapter的前景模式;
@Override
protected void onPause()
{
super.onPause();
gResumed = false;
// 由於NfcAdapter啟動前景模式將相對花費更多的電力,要記得關閉。
gNfcAdapter.disableForegroundNdefPush(this);
}
[範例程式]
======
按照上方的說明應能實作出與NFC tag、應用程式交換資料的範例程式。該文章主要擷錄<StickyNotes sample code>的內容,
有更多相關的程式細節還有待補充與了解。如果有撰寫錯誤的地方,也請大家多多指教,謝謝。
References:
‧NFC Demo - Android sample code
‧Near Field Communication (重要)
‧Android NFC 开发教程(2): ApiDemos->NFC->ForegoundDispatch
‧Android NFC 開發教程(3): Mifare Tag 讀寫示例
‧NFC Programming in Android & [Android] 簡單範例: NFC Push
‧Developer Document & NXP TagWriter & StickyNotes sample code
‧onNewIntent调用时机 & onNewIntent的应用 & Activity - onNewIntent()