บทเรียนเบื้องต้นสำหรับนักพัฒนาแอพพลิเคชันบนแพลตฟอร์ม Android กับการอ่านค่า NFC เบื้องต้น, สิ่งที่ต้องใช้ NFc Tag, Android Studio และสมาร์ทโฟนที่รองรับ NFC
บทเรียนก่อนหน้านี้: บทเรียนการ เขียนแอพ Android
ตัวอย่างนี้ต้องใช้การทดสอบบน สมาร์ทโฟนเครื่องที่รองรับ NFC เท่านั้นครับ และก็การเขียนหรือ Writer Tag ลง NFC ที่ซื้อมา ราคา 49, 69 บาท หรือถูกๆ ก็ 20 กว่าบาท ก็ใช้ แอพฯอะไรก็ได้เขียนลงไปก่อน
ส่วนการพัฒนานั้นประยุกต์ใช้กับ บทความในการพัฒนาร่วมกับ http://www.nfc-forum.org/specs/
เริ่มต้นทดสอบ และพัฒนา
ให้เราใช้แอพพลิเคชันอะไรก็ได้เขียนข้อมูลลงไปใน NFC Tag ที่เราซื้อมาครับ ในตัวอย่าง ผมใช้การเขียน Tag ของ URL Facebook ของผมลงไป ซึ่งใช้ข้อมูลไม่มาก 0.26 KB เอง
นี่คือ NFC ราคาถูกๆ ที่เราซื้อมา (แบบ Tap หรือแบบทาบนะครับ)
ตัวอย่างนี้ใช้แอพฯNFC Tools บน PlayStore เขียนข้อมูล http://www.facebook.com/banyapon ลงไป
ต่อมาให้เราเปิด Android Studio ครับสร้าง New Project เป็น Blank Activity ขึ้นมาเลย สำหรับให้เราเริ่มพัฒนาแอพพลิเคชันของเรา ในตัวอย่างนี้ แนะนำว่าต้องพัฒนา และทดสอบบน อุปกรณ์จริงเท่านั้น
เมื่อสร้างโปรเจ็คใหม่ของเราเรียบร้อย เปิดไฟล์ AndroidManifest.xml ครับ เพิ่ม Permission เข้าไป
<uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" />
ดังนั้น ภาพรวมไฟล์ AndroidManifest.xml จะเป็นดังนี้
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="th.ac.dpu.nfc_dpu" > <uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
เปิดไฟล์ res/strings.xml ขึ้นมาครับ
แก้ไขไฟล์ strings.xml ตามนี้ครับ
<resources> <string name="app_name">DPU NFC Reader</string> <string name="text_description">แตะโทรศัพท์ที่ NFC Tag เพื่ออ่านค่า Facebook</string> <string name="action_settings">Settings</string> </resources>
เพื่อเพิ่มชุด String เข้าไปชื่อว่า “text_description” สำหรับเป็นชุด String อธิบายวิธีการทำงานของแอพเรา
สุดท้ายไปออกแบบหน้า Layout กันที่ไฟล์ content_main.xml แก้ไขดังนี้ครับ
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:showIn="@layout/activity_main" tools:context=".MainActivity"> <TextView android:id="@+id/resultText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/text_description" /> </RelativeLayout>
มีเพิ่มแค่ส่วนเดียวเท่านั้น
<TextView android:id="@+id/resultText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/text_description" />
สุดท้ายคือการแก้ไขไฟล์ใน MainActivity.java ครับ
ส่วนของ Import Header คือ
import android.app.Activity; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.Menu; import android.view.MenuItem; import java.io.UnsupportedEncodingException; import java.util.Arrays; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentFilter.MalformedMimeTypeException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.tech.Ndef; import android.os.AsyncTask; import android.util.Log; import android.widget.TextView; import android.widget.Toast;
ประกาศ Global Variable
public static final String MIME_TEXT_PLAIN = "text/plain"; public static final String TAG = "CheckNFC"; private TextView mTextView; private NfcAdapter mNfcAdapter;
เพื่อเรียกใช้ NfcAdapter มาทำงาน โดยจะมีการเชื่อมกับอุปกรณ์โดยตรงได้เอง ต่อมาก็ onCreate();
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mTextView = (TextView) findViewById(R.id.resultText); mNfcAdapter = NfcAdapter.getDefaultAdapter(this); if (mNfcAdapter == null) { Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show(); finish(); return; } if (!mNfcAdapter.isEnabled()) { mTextView.setText("NFC is disabled."); } else { mTextView.setText(R.string.text_description); } NFCIntent(getIntent()); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } }); }
เมื่อรับค่าผ่านจะเรียกใช้ฟังก์ชันเมธอด NFCIntent เพื่อเปลี่ยนหน้าไปอ่านค่าใดๆ
private void NFCIntent(Intent intent) { String action = intent.getAction(); if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { String type = intent.getType(); if (MIME_TEXT_PLAIN.equals(type)) { Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); new NdefReaderTask().execute(tag); } else { Log.d(TAG, "Wrong mime type: " + type); } } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) { // In case we would still use the Tech Discovered Intent Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); String[] techList = tag.getTechList(); String searchedTech = Ndef.class.getName(); for (String tech : techList) { if (searchedTech.equals(tech)) { new NdefReaderTask().execute(tag); break; } } } }
เพิ่มส่วนของการทำงานจาก nfc-forum เข้าไปครับ
public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) { final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass()); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0); IntentFilter[] filters = new IntentFilter[1]; String[][] techList = new String[][]{}; // Notice that this is the same filter as in our manifest. filters[0] = new IntentFilter(); filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED); filters[0].addCategory(Intent.CATEGORY_DEFAULT); try { filters[0].addDataType(MIME_TEXT_PLAIN); } catch (MalformedMimeTypeException e) { throw new RuntimeException("Check your mime type."); } adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList); }
สุดท้ายคือส่วนที่ทำงานแบบ Background ที่เว็บ Forum เค้าพัฒนากันมานานสักพักแล้ว
@Override protected void onResume() { super.onResume(); setupForegroundDispatch(this, mNfcAdapter); } @Override protected void onPause() { stopForegroundDispatch(this, mNfcAdapter); super.onPause(); } @Override protected void onNewIntent(Intent intent) { NFCIntent(intent); } public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) { adapter.disableForegroundDispatch(activity); } /** * Background task for reading the data. Do not block the UI thread while reading. * * @author Ralf Wondratschek * */ private class NdefReaderTask extends AsyncTask<Tag, Void, String> { @Override protected String doInBackground(Tag... params) { Tag tag = params[0]; Ndef ndef = Ndef.get(tag); if (ndef == null) { // NDEF is not supported by this Tag. return null; } NdefMessage ndefMessage = ndef.getCachedNdefMessage(); NdefRecord[] records = ndefMessage.getRecords(); for (NdefRecord ndefRecord : records) { if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) { try { return readText(ndefRecord); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Unsupported Encoding", e); } } } return null; } private String readText(NdefRecord record) throws UnsupportedEncodingException { /* * See NFC forum specification for "Text Record Type Definition" at 3.2.1 * * http://www.nfc-forum.org/specs/ * * bit_7 defines encoding * bit_6 reserved for future use, must be 0 * bit_5..0 length of IANA language code */ byte[] payload = record.getPayload(); // Get the Text Encoding String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"; // Get the Language Code int languageCodeLength = payload[0] & 0063; // String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII"); // e.g. "en" // Get the Text return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); } @Override protected void onPostExecute(String result) { if (result != null) { mTextView.setText("Read content: " + result); } }
ภาพรวมของไฟล์ MainActivity.java เป็นดังนี้
package th.ac.dpu.nfc_dpu; import android.app.Activity; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.view.Menu; import android.view.MenuItem; import java.io.UnsupportedEncodingException; import java.util.Arrays; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentFilter.MalformedMimeTypeException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.tech.Ndef; import android.os.AsyncTask; import android.util.Log; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends AppCompatActivity { public static final String MIME_TEXT_PLAIN = "text/plain"; public static final String TAG = "CheckNFC"; private TextView mTextView; private NfcAdapter mNfcAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mTextView = (TextView) findViewById(R.id.resultText); mNfcAdapter = NfcAdapter.getDefaultAdapter(this); if (mNfcAdapter == null) { Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show(); finish(); return; } if (!mNfcAdapter.isEnabled()) { mTextView.setText("NFC is disabled."); } else { mTextView.setText(R.string.text_description); } NFCIntent(getIntent()); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } private void NFCIntent(Intent intent) { String action = intent.getAction(); if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { String type = intent.getType(); if (MIME_TEXT_PLAIN.equals(type)) { Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); new NdefReaderTask().execute(tag); } else { Log.d(TAG, "Wrong mime type: " + type); } } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) { // In case we would still use the Tech Discovered Intent Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); String[] techList = tag.getTechList(); String searchedTech = Ndef.class.getName(); for (String tech : techList) { if (searchedTech.equals(tech)) { new NdefReaderTask().execute(tag); break; } } } } /** * @param activity The corresponding {@link Activity} requesting the foreground dispatch. * @param adapter The {@link NfcAdapter} used for the foreground dispatch. */ public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) { final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass()); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0); IntentFilter[] filters = new IntentFilter[1]; String[][] techList = new String[][]{}; // Notice that this is the same filter as in our manifest. filters[0] = new IntentFilter(); filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED); filters[0].addCategory(Intent.CATEGORY_DEFAULT); try { filters[0].addDataType(MIME_TEXT_PLAIN); } catch (MalformedMimeTypeException e) { throw new RuntimeException("Check your mime type."); } adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList); } @Override protected void onResume() { super.onResume(); setupForegroundDispatch(this, mNfcAdapter); } @Override protected void onPause() { stopForegroundDispatch(this, mNfcAdapter); super.onPause(); } @Override protected void onNewIntent(Intent intent) { NFCIntent(intent); } public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) { adapter.disableForegroundDispatch(activity); } /** * Background task for reading the data. Do not block the UI thread while reading. * * @author Ralf Wondratschek * */ private class NdefReaderTask extends AsyncTask<Tag, Void, String> { @Override protected String doInBackground(Tag... params) { Tag tag = params[0]; Ndef ndef = Ndef.get(tag); if (ndef == null) { // NDEF is not supported by this Tag. return null; } NdefMessage ndefMessage = ndef.getCachedNdefMessage(); NdefRecord[] records = ndefMessage.getRecords(); for (NdefRecord ndefRecord : records) { if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) { try { return readText(ndefRecord); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Unsupported Encoding", e); } } } return null; } private String readText(NdefRecord record) throws UnsupportedEncodingException { /* * See NFC forum specification for "Text Record Type Definition" at 3.2.1 * * http://www.nfc-forum.org/specs/ * * bit_7 defines encoding * bit_6 reserved for future use, must be 0 * bit_5..0 length of IANA language code */ byte[] payload = record.getPayload(); // Get the Text Encoding String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"; // Get the Language Code int languageCodeLength = payload[0] & 0063; // String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII"); // e.g. "en" // Get the Text return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); } @Override protected void onPostExecute(String result) { if (result != null) { mTextView.setText("Read content: " + result); } } } }
มาทดสอบกันครับ
รัน Project ของเราลงเครื่องที่ต้องการทดสอบครับ แล้วนำไปทาบที่ NFC Tag ที่เราเขียนไว้แล้วได้เลย เราจะเห็นว่า แอพ ของเราจะ Intent ไปยัง Facebook ของเรานั่นคือ URL ที่ผมเขียนไว้ครับ
วีดีโอตัวอย่างผลลัพธ์
Source Code สามารถ Clone Git ได้เลย:
เป็นไงครับไม่ยากอีกแล้วใช่ไหม;
เพิ่มเติม: ขอความกรุณา พี่น้องนักเรียนนักศึกษา และผู้ที่สนใจพัฒนาเกมหรือแอพฯใดๆ ที่เป็น Friends หรืออยู่ใน Group นักพัฒนาต่างๆ หน่อยนะครับ บทเรียนหลายๆ บทเรียนผมศึกษาจากเว็บต่างประเทศบ้าง มาประยุกต์มาปรับ การเขียนให้ ส่วนมากก็คือการไปศึกษามา ทีนี้!, น้องๆ หลายๆ คน และคนรู้จักบน Facebook ของผม และใน Group ก็ส่วนมากเวลาติดอะไร นิดๆ หน่อยๆ ก็จะถามใน Message หรือ ถาม บน Group เลย บางทีเป็นเรื่องใช้ความพยายามเล็กน้อยในการค้นหาก็แก้ปัญหาได้แล้วครับ แต่การถามตลอดเวลา พอไม่ตอบก็เร่งมามันเป็นการ Annoy หรือรบกวนไปแล้วครับ ฝากเรื่องนี้แหละครับ รบกวนก่อนจะสอบถามอะไร ยินดีช่วยแก้ปัญหาครับ แต่อยากให้พยายามในการค้นหา และแก้ปัญหาด้วยตัวเองให้ได้ก่อนครับ ขอบคุณครับ 🙂