Чтение ярлыков КБП (Коммуникация ближнего поля) с Android
() translation by (you can also view the original English article)
Вам интересно, что такое КБП и как его можно интегрировать в ваши собственные приложения для Android? Этот учебник быстро познакомит вас с этой темой, прежде чем погрузиться в нее и научит создавать простое приложение для чтения КБП!
Что такое КБП?
КБП - это сокращение от Коммуникация ближнего поля (NFC -Near Field Communication). Это международный стандарт для бесконтактного обмена данными. В отличие от большой спектр других технологий, таких как беспроводной локальной сети и Bluetooth максимальное расстояние двух устройств составляет 10 см. Разработка стандарта началась в 2002 году NXP Semiconductors, Sony. КБП Форум, консорциум из более 170 компаний и членов, в который входят Mastercard, NXP, Nokia, Samsung, Intel и Google, разрабатывает новые спецификации с 2004 года.
Существуют различные возможности использования КБП с мобильными устройствами; например, безбумажные билеты, средства контроля доступа, безналичные платежи и ключи от машины. С помощью ярлыков КБП вы можете управлять своим телефоном и изменять настройки. Данные можно обменивать, просто держа два устройства рядом друг с другом.
В этом уроке я хочу объяснить, как реализовать КБП с Android SDK, какие подводные камни существуют и что нужно иметь в виду. Мы шаг за шагом создадим приложение, которое сможет читать содержимое ярлыков КБП, поддерживающих NDEF.
КБП-технологии
Существует множество ярлыков КБП, которые можно прочитать с помощью смартфона. Спектр варьируется от простых наклеек и брелков до сложных карт со встроенным криптографическим оборудованием. Ярлыки также отличаются по своей технологии чипов. Наиболее важным является NDEF, который поддерживается большинством ярлыков. Кроме того, следует упомянуть Mifare, поскольку это самая распространенная технология бесконтактных чипов в мире. Некоторые ярлыки могут быть прочитаны и записаны, в то время как другие доступны только для чтения или зашифрованы.
В этом руководстве обсуждается только формат обмена данными КБП (NDEF).



Добавление поддержки КБП в приложение
Мы начинаем с нового проекта и нулевой деятельности. Важно выбрать минимальную версию SDK уровня 10, потому что КБП поддерживается только после Android 2.3.3. Не забудьте выбрать имя пакета. Я выбрал net.vrallev.android.nfc.demo, потому что vrallev.net является доменом моего сайта, а другая часть относится к теме этого приложения.
1 |
|
2 |
<uses-sdk |
3 |
android:minSdkVersion="10" |
4 |
android:targetSdkVersion="17" /> |
Макет по умолчанию, сгенерированный Eclipse, для нас почти достаточен. Я только добавил ID к TextView и изменил текст.
1 |
|
2 |
<TextView |
3 |
android:id="@+id/textView_explanation" |
4 |
android:layout_width="wrap_content" |
5 |
android:layout_height="wrap_content" |
6 |
android:text="@string/explanation" /> |
Чтобы получить доступ к оборудованию КБП, вы должны обратиться за разрешением в манифест. Если приложение не будет работать без КБП, вы можете указать состояние с помощью ярлыка метки использования. Если требуется КБП, приложение не может быть установлено на устройство без него, и Google Play будет отображать ваше приложение только пользователям, которые владеют устройством КБП.
1 |
|
2 |
<uses-permission android:name="android.permission.NFC" /> |
3 |
|
4 |
<uses-feature |
5 |
android:name="android.hardware.nfc" |
6 |
android:required="true" /> |
MainActivity должен состоять только методом onCreate (). Вы можете взаимодействовать с оборудованием через класс NfcAdapter. Важно выяснить, является ли NfcAdapter нулевым. В этом случае устройство Android не поддерживает КБП.
1 |
|
2 |
package net.vrallev.android.nfc.demo; |
3 |
|
4 |
import android.app.Activity; |
5 |
import android.nfc.NfcAdapter; |
6 |
import android.os.Bundle; |
7 |
import android.widget.TextView; |
8 |
import android.widget.Toast; |
9 |
|
10 |
/**
|
11 |
* Activity for reading data from an NDEF Tag.
|
12 |
*
|
13 |
* @author Ralf Wondratschek
|
14 |
*
|
15 |
*/
|
16 |
public class MainActivity extends Activity { |
17 |
|
18 |
public static final String TAG = "NfcDemo"; |
19 |
|
20 |
private TextView mTextView; |
21 |
private NfcAdapter mNfcAdapter; |
22 |
|
23 |
@Override |
24 |
protected void onCreate(Bundle savedInstanceState) { |
25 |
super.onCreate(savedInstanceState); |
26 |
setContentView(R.layout.activity_main); |
27 |
|
28 |
mTextView = (TextView) findViewById(R.id.textView_explanation); |
29 |
|
30 |
mNfcAdapter = NfcAdapter.getDefaultAdapter(this); |
31 |
|
32 |
if (mNfcAdapter == null) { |
33 |
// Stop here, we definitely need NFC
|
34 |
Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show(); |
35 |
finish(); |
36 |
return; |
37 |
|
38 |
}
|
39 |
|
40 |
if (!mNfcAdapter.isEnabled()) { |
41 |
mTextView.setText("NFC is disabled."); |
42 |
} else { |
43 |
mTextView.setText(R.string.explanation); |
44 |
}
|
45 |
|
46 |
handleIntent(getIntent()); |
47 |
}
|
48 |
|
49 |
private void handleIntent(Intent intent) { |
50 |
// TODO: handle Intent
|
51 |
}
|
52 |
}
|
Если мы запустим наше приложение сейчас, мы увидим текст, включен ли КБП или нет.
Как отфильтровать ярлыки КБП
У нас есть пример приложения и мы хотим получать уведомление от системы, когда мы прикрепляем к устройству ярлык КБП. Как обычно, Android использует систему Intent для доставки ярлыков для приложения. Если несколько приложений могут обрабатывать Intent, отображается окно выбора активности, и пользователь может решить, какое приложение будет открыто. Открытие URL или обмен информацией обрабатываются одинаково.
КБП Intent фильтр
Существует три различные фильтры для ярлыков:
- ACTION_NDEF_DISCOVERED
- ACTION_TECH_DISCOVERED
- ACTION_TAG_DISCOVERED
Список отсортирован от самого высокого до самого низкого приоритета.
Что происходит, когда к смартфону прикрепляется ярлык? Если система обнаруживает ярлык с поддержкой NDEF, запускается Intent. ACTION_TECH_DISCOVERED Intent (намерением) инициируется, если нет активности от любого приложения регистрируется для NDEF Intent (намерения) или ярлык не поддерживает NDEF . Если снова не найдено приложение для Intent или технология чипа не может быть обнаружена, то запускается Intent (намерение) ACTION_TAG_DISCOVERED. На следующем рисунке показан процесс:



В целом это означает, что каждое приложение необходимо фильтровать после Intent (намерения) с наивысшим приоритетом. В нашем случае это NDEF Intent (намерение). Сначала мы реализуем Intent (намерение) ACTION_TECH_DISCOVERED, чтобы выделить разницу между приоритетами.
Техника обнаружила Intent (намерения)
Мы должны указать интересующую нас технологию. Для этого мы создаем подпапку с именем xml в папке res. В этой папке мы создаем файл nfc_tech_filter.xml, в котором мы указываем технологии.
1 |
|
2 |
<?xml version="1.0" encoding="utf-8"?>
|
3 |
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> |
4 |
<tech-list>
|
5 |
<tech>android.nfc.tech.Ndef</tech> |
6 |
<!-- class name -->
|
7 |
</tech-list>
|
8 |
</resources>
|
9 |
|
10 |
<!--
|
11 |
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
12 |
<tech-list>
|
13 |
<tech>android.nfc.tech.IsoDep</tech>
|
14 |
<tech>android.nfc.tech.NfcA</tech>
|
15 |
<tech>android.nfc.tech.NfcB</tech>
|
16 |
<tech>android.nfc.tech.NfcF</tech>
|
17 |
<tech>android.nfc.tech.NfcV</tech>
|
18 |
<tech>android.nfc.tech.Ndef</tech>
|
19 |
<tech>android.nfc.tech.NdefFormatable</tech>
|
20 |
<tech>android.nfc.tech.MifareClassic</tech>
|
21 |
<tech>android.nfc.tech.MifareUltralight</tech>
|
22 |
</tech-list>
|
23 |
</resources>
|
24 |
-->
|
Теперь мы должны создать IntentFilter в манифесте, и приложение будет запущено, когда мы прикрепим ярлык.
1 |
|
2 |
<?xml version="1.0" encoding="utf-8"?>
|
3 |
<activity
|
4 |
android:name="net.vrallev.android.nfc.demo.MainActivity" |
5 |
android:label="@string/app_name" > |
6 |
<intent-filter>
|
7 |
<action android:name="android.intent.action.MAIN" /> |
8 |
<category android:name="android.intent.category.LAUNCHER" /> |
9 |
</intent-filter>
|
10 |
|
11 |
<intent-filter>
|
12 |
<action android:name="android.nfc.action.TECH_DISCOVERED" /> |
13 |
</intent-filter>
|
14 |
|
15 |
<meta-data
|
16 |
android:name="android.nfc.action.TECH_DISCOVERED" |
17 |
android:resource="@xml/nfc_tech_filter" /> |
18 |
</activity>
|
Если никакое другое приложение не зарегистрировано для этого Intent (намерения), наша Деятельность начнется немедленно. На моем устройстве, однако, установлены другие приложения, поэтому отображается выбор активности.



NDEF обнаружил намерения
Как я упоминал ранее, техника которая обнаружила Намерения есть второй самый высокий приоритет. Однако, поскольку наше приложение будет поддерживать только NDEF, вместо этого мы можем использовать обнаруженное намерение NDEF, которое имеет более высокий приоритет. Мы можем снова удалить список технологий и заменить следующие IntentFilter.
1 |
|
2 |
<intent-filter> |
3 |
<action android:name="android.nfc.action.NDEF_DISCOVERED" /> |
4 |
|
5 |
<category android:name="android.intent.category.DEFAULT" /> |
6 |
|
7 |
<data android:mimeType="text/plain" /> |
8 |
</intent-filter> |
Когда мы сейчас прикрепим ярлык, приложение будет запущено, как и раньше. Однако для меня есть разница. Средство выбора действий не отображается, и приложение запускается немедленно, поскольку у намерения NDEF более высокий приоритет, а у других приложений зарегистрирован только более низкий приоритет. Это именно то, что мы хотим.
Передний план Отправка
Обратите внимание, что одна проблема остается. Когда наше приложение уже открыто, и мы снова прикрепляем тег, приложение открывается второй раз вместо прямой доставки тега. Это не наше предполагаемое поведение. Вы можете обойти проблему с помощью Foreground Dispatch (переднего плана).
Вместо того, чтобы система распространила Намерение, вы можете зарегистрировать свою активность, чтобы получить ярлык напрямую. Это важно для определенного рабочего процесса, когда нет смысла открывать другое приложение.
Я вставил объяснения в соответствующие места в коде.
1 |
|
2 |
package net.vrallev.android.nfc.demo; |
3 |
|
4 |
import android.app.Activity; |
5 |
import android.app.PendingIntent; |
6 |
import android.content.Intent; |
7 |
import android.content.IntentFilter; |
8 |
import android.content.IntentFilter.MalformedMimeTypeException; |
9 |
import android.nfc.NfcAdapter; |
10 |
import android.os.Bundle; |
11 |
import android.widget.TextView; |
12 |
import android.widget.Toast; |
13 |
|
14 |
/**
|
15 |
* Activity for reading data from an NDEF Tag.
|
16 |
*
|
17 |
* @author Ralf Wondratschek
|
18 |
*
|
19 |
*/
|
20 |
public class MainActivity extends Activity { |
21 |
|
22 |
public static final String MIME_TEXT_PLAIN = "text/plain"; |
23 |
public static final String TAG = "NfcDemo"; |
24 |
|
25 |
private TextView mTextView; |
26 |
private NfcAdapter mNfcAdapter; |
27 |
|
28 |
@Override |
29 |
protected void onCreate(Bundle savedInstanceState) { |
30 |
super.onCreate(savedInstanceState); |
31 |
setContentView(R.layout.activity_main); |
32 |
|
33 |
mTextView = (TextView) findViewById(R.id.textView_explanation); |
34 |
|
35 |
mNfcAdapter = NfcAdapter.getDefaultAdapter(this); |
36 |
|
37 |
if (mNfcAdapter == null) { |
38 |
// Stop here, we definitely need NFC
|
39 |
Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show(); |
40 |
finish(); |
41 |
return; |
42 |
|
43 |
}
|
44 |
|
45 |
if (!mNfcAdapter.isEnabled()) { |
46 |
mTextView.setText("NFC is disabled."); |
47 |
} else { |
48 |
mTextView.setText(R.string.explanation); |
49 |
}
|
50 |
|
51 |
handleIntent(getIntent()); |
52 |
}
|
53 |
|
54 |
@Override |
55 |
protected void onResume() { |
56 |
super.onResume(); |
57 |
|
58 |
/**
|
59 |
* It's important, that the activity is in the foreground (resumed). Otherwise
|
60 |
* an IllegalStateException is thrown.
|
61 |
*/
|
62 |
setupForegroundDispatch(this, mNfcAdapter); |
63 |
}
|
64 |
|
65 |
@Override |
66 |
protected void onPause() { |
67 |
/**
|
68 |
* Call this before onPause, otherwise an IllegalArgumentException is thrown as well.
|
69 |
*/
|
70 |
stopForegroundDispatch(this, mNfcAdapter); |
71 |
|
72 |
super.onPause(); |
73 |
}
|
74 |
|
75 |
@Override |
76 |
protected void onNewIntent(Intent intent) { |
77 |
/**
|
78 |
* This method gets called, when a new Intent gets associated with the current activity instance.
|
79 |
* Instead of creating a new activity, onNewIntent will be called. For more information have a look
|
80 |
* at the documentation.
|
81 |
*
|
82 |
* In our case this method gets called, when the user attaches a Tag to the device.
|
83 |
*/
|
84 |
handleIntent(intent); |
85 |
}
|
86 |
|
87 |
private void handleIntent(Intent intent) { |
88 |
// TODO: handle Intent
|
89 |
}
|
90 |
|
91 |
/**
|
92 |
* @param activity The corresponding {@link Activity} requesting the foreground dispatch.
|
93 |
* @param adapter The {@link NfcAdapter} used for the foreground dispatch.
|
94 |
*/
|
95 |
public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) { |
96 |
final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass()); |
97 |
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); |
98 |
|
99 |
final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0); |
100 |
|
101 |
IntentFilter[] filters = new IntentFilter[1]; |
102 |
String[][] techList = new String[][]{}; |
103 |
|
104 |
// Notice that this is the same filter as in our manifest.
|
105 |
filters[0] = new IntentFilter(); |
106 |
filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED); |
107 |
filters[0].addCategory(Intent.CATEGORY_DEFAULT); |
108 |
try { |
109 |
filters[0].addDataType(MIME_TEXT_PLAIN); |
110 |
} catch (MalformedMimeTypeException e) { |
111 |
throw new RuntimeException("Check your mime type."); |
112 |
}
|
113 |
|
114 |
adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList); |
115 |
}
|
116 |
|
117 |
/**
|
118 |
* @param activity The corresponding {@link BaseActivity} requesting to stop the foreground dispatch.
|
119 |
* @param adapter The {@link NfcAdapter} used for the foreground dispatch.
|
120 |
*/
|
121 |
public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) { |
122 |
adapter.disableForegroundDispatch(activity); |
123 |
}
|
124 |
}
|
Теперь, когда вы прикрепляете тег, и наше приложение уже открыто, вызывается onNewIntent, и новая активность не создается.
Чтение данных из ярлыка NDEF
Последний шаг - прочитать данные из ярлыка. Объяснения вставляются в соответствующие места в коде еще раз. NdefReaderTask - это закрытый внутренний класс.
1 |
|
2 |
package net.vrallev.android.nfc.demo; |
3 |
|
4 |
import java.io.UnsupportedEncodingException; |
5 |
import java.util.Arrays; |
6 |
|
7 |
import android.app.Activity; |
8 |
import android.app.PendingIntent; |
9 |
import android.content.Intent; |
10 |
import android.content.IntentFilter; |
11 |
import android.content.IntentFilter.MalformedMimeTypeException; |
12 |
import android.nfc.NdefMessage; |
13 |
import android.nfc.NdefRecord; |
14 |
import android.nfc.NfcAdapter; |
15 |
import android.nfc.Tag; |
16 |
import android.nfc.tech.Ndef; |
17 |
import android.os.AsyncTask; |
18 |
import android.os.Bundle; |
19 |
import android.util.Log; |
20 |
import android.widget.TextView; |
21 |
import android.widget.Toast; |
22 |
|
23 |
/*
|
24 |
* ... other code parts
|
25 |
*/
|
26 |
|
27 |
private void handleIntent(Intent intent) { |
28 |
String action = intent.getAction(); |
29 |
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { |
30 |
|
31 |
String type = intent.getType(); |
32 |
if (MIME_TEXT_PLAIN.equals(type)) { |
33 |
|
34 |
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); |
35 |
new NdefReaderTask().execute(tag); |
36 |
|
37 |
} else { |
38 |
Log.d(TAG, "Wrong mime type: " + type); |
39 |
}
|
40 |
} else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) { |
41 |
|
42 |
// In case we would still use the Tech Discovered Intent
|
43 |
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); |
44 |
String[] techList = tag.getTechList(); |
45 |
String searchedTech = Ndef.class.getName(); |
46 |
|
47 |
for (String tech : techList) { |
48 |
if (searchedTech.equals(tech)) { |
49 |
new NdefReaderTask().execute(tag); |
50 |
break; |
51 |
}
|
52 |
}
|
53 |
}
|
54 |
}
|
1 |
|
2 |
/**
|
3 |
* Background task for reading the data. Do not block the UI thread while reading.
|
4 |
*
|
5 |
* @author Ralf Wondratschek
|
6 |
*
|
7 |
*/
|
8 |
private class NdefReaderTask extends AsyncTask<Tag, Void, String> { |
9 |
|
10 |
@Override |
11 |
protected String doInBackground(Tag... params) { |
12 |
Tag tag = params[0]; |
13 |
|
14 |
Ndef ndef = Ndef.get(tag); |
15 |
if (ndef == null) { |
16 |
// NDEF is not supported by this Tag.
|
17 |
return null; |
18 |
}
|
19 |
|
20 |
NdefMessage ndefMessage = ndef.getCachedNdefMessage(); |
21 |
|
22 |
NdefRecord[] records = ndefMessage.getRecords(); |
23 |
for (NdefRecord ndefRecord : records) { |
24 |
if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) { |
25 |
try { |
26 |
return readText(ndefRecord); |
27 |
} catch (UnsupportedEncodingException e) { |
28 |
Log.e(TAG, "Unsupported Encoding", e); |
29 |
}
|
30 |
}
|
31 |
}
|
32 |
|
33 |
return null; |
34 |
}
|
35 |
|
36 |
private String readText(NdefRecord record) throws UnsupportedEncodingException { |
37 |
/*
|
38 |
* See NFC forum specification for "Text Record Type Definition" at 3.2.1
|
39 |
*
|
40 |
* http://www.nfc-forum.org/specs/
|
41 |
*
|
42 |
* bit_7 defines encoding
|
43 |
* bit_6 reserved for future use, must be 0
|
44 |
* bit_5..0 length of IANA language code
|
45 |
*/
|
46 |
|
47 |
byte[] payload = record.getPayload(); |
48 |
|
49 |
// Get the Text Encoding
|
50 |
String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"; |
51 |
|
52 |
// Get the Language Code
|
53 |
int languageCodeLength = payload[0] & 0063; |
54 |
|
55 |
// String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
|
56 |
// e.g. "en"
|
57 |
|
58 |
// Get the Text
|
59 |
return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); |
60 |
}
|
61 |
|
62 |
@Override |
63 |
protected void onPostExecute(String result) { |
64 |
if (result != null) { |
65 |
mTextView.setText("Read content: " + result); |
66 |
}
|
67 |
}
|
68 |
}
|
Теперь приложение успешно считывает содержимое.



Полезные приложения
Чтобы проверить, правильно ли считываются и записываются данные, мне лично нравятся следующие приложения:
- КБП TagInfo от NFC Research Lab для чтения данных
- TagInfo от NXP SEMICONDUCTORS для чтения данных
- TagWriter от NXP SEMICONDUCTORS для записи данных
Заключение
В этом уроке я показал вам, как можно извлечь данные из тега NDEF. Вы можете распространить пример на другие похожие типы и технологии чипов; функция записи данных также будет полезна. Первый шаг для работы с NFC был сделан. Тем не менее, Android SDK предлагает гораздо больше возможностей, таких как простой обмен данными (называемый Android Beam).
Об авторе
Ральф Вондрачек - студент информатики из Германии. Помимо учебы, Ральф работает в новшествах, которые и предлагает, в области мобильных компьютеров. В последние несколько лет он работал с Java, XML, HTML, JSP, JSF, Eclipse, Google App Engine и, конечно, с Android. На сегодняшний день он опубликовал два приложения для Android, которые можно найти здесь.
Вы можете узнать больше о работе автора на его домашней странице vrallev.net.
Источники
http://www.nfc-forum.org/home/n-mark.jpg
http://commons.wikimedia.org/wiki/File%3A%C3%9Cberlagert.jpg
http://developer.android.com/images/nfc_tag_dispatch.png