Статьи

Изучите основы Android NFC, создав простой мессенджер

Эта статья была рецензирована Тимом Севериеном . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


NFC (Near Field Communication) — это беспроводной метод связи на коротком расстоянии между устройствами или между устройством и меткой NFC. NFC не ограничивается Android или мобильными устройствами в целом, но это руководство предназначено для реализации NFC в Android.

К концу этого урока вы поймете основные понятия NFC, а также как настроить базовую связь между устройствами Android. Вам нужно иметь API 14 или выше, чтобы пройти этот урок. Хотя некоторые функции, представленные в API 16, используются, они являются вспомогательными функциями и не требуются.

Вы можете найти полный код этого руководства на GitHub .

Форматирование для NFC

NFC имеет общий стандарт, созданный NFC Forum для обеспечения того, чтобы интерфейс мог работать в разных системах. Этот формат — «NDEF» (формат обмена данными NFC), он позволяет нам знать, как информация в тегах представляется нам, и дает возможность гарантировать, что создаваемые нами данные могут быть полезны как можно большему числу пользователей.

Пока это все, что я скажу о форматировании, но вернемся к этому.

Система диспетчеризации тегов

ОС Android обрабатывает NFC через свою «Систему диспетчеризации тегов NFC». Это часть системы, отдельная от вашего приложения, над которой у вас мало контроля. Он постоянно ищет (при условии, что NFC не отключен на устройстве) для устройств NFC, с которыми он может взаимодействовать. Если устройство находится в пределах 4 сантиметров от другого устройства с поддержкой NFC или тега NFC, система отправит намерение, и именно так мы получаем данные.

Откройте Android Studio и создайте проект с пустым действием, и мы начнем.

Фильтрация для NFC Intents

При фильтрации для намерений NFC вы хотите быть как можно более конкретным. Это сделано для того, чтобы избежать появления диалогового окна выбора приложений, которые могут обрабатывать намерения. Обычно, выбор не является проблемой, и часто это предпочтительное поведение, чтобы позволить ему (или заставить его показать), но это не относится к NFC.

NFC требует, чтобы устройства были в пределах сантиметров друг от друга. Если мы разрешим показу выбора, наш пользователь, скорее всего, вернет устройство обратно к себе, чтобы посмотреть и отменить взаимодействие.

Система диспетчеризации тегов имеет три действия, которые она может присоединить к намерению, которое она создала в ответ на поиск чего-либо, с чем она может читать, писать или общаться. Система диспетчеризации отправит несколько намерений, если одному намерению не удается найти действие для его обработки, то отправляется следующее в списке.

Действия, которые система диспетчеризации тегов применит к своим намерениям:

  1. ACTION_NDEF_DISCOVERED — отправляется, если найденная информация отформатирована как NDEF.
  2. ACTION_TECH_DISCOVERED — отправляется, если первый сбой или если данные были отформатированы незнакомым способом
  3. ACTION_TAG_DISCOVERED — последний и самый общий. Помните, что мы хотим зафиксировать намерение до этого, так как, скорее всего, у нас будет несколько действий, которые указали что-то общее

Мы собираемся создать простой мессенджер для отправки и получения списка строк.

Откройте AndroidManifest.xml и добавьте следующий фильтр намерений в основное действие:

 <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT"/> <data android:mimeType="text/plain" /> </intent-filter> 

Как и в любом Android-проекте, нам нужно будет запросить соответствующие разрешения. Добавьте следующее разрешение и функцию в AndroidManifest.xml :

 <uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true"/> 

Чтобы потом все было более гладко, установите режим запуска основного действия на «одиночная задача». Это позволит нам обрабатывать намерения, отправленные в нашу деятельность, без необходимости воссоздавать активность, что дает более плавное ощущение нашему пользователю.

 <activity android:launchMode="singleTask" android:name=".MainActivity" android:label="@string/app_name" > 

Простой интерфейс

Нам нужен способ добавления сообщений для отправки в массив строк. Вы можете создать свой собственный метод или использовать простой интерфейс, который у меня есть ниже.

Измените activity_main.xml на:

 <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:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <EditText android:id="@+id/txtBoxAddMessage" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/buttonAddMessage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="addMessage" android:layout_below="@+id/txtBoxAddMessage" android:layout_centerHorizontal="true" /> <TextView android:id="@+id/txtMessagesReceived" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/buttonAddMessage" android:layout_alignParentEnd="true" android:layout_alignParentRight="true"/> <TextView android:id="@+id/txtMessageToSend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@+id/txtMessagesReceived" android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/> </RelativeLayout> 

Обновите MainActivity.java следующим образом:

 public class MainActivity extends AppCompatActivity { //The array lists to hold our messages private ArrayList<String> messagesToSendArray = new ArrayList<>(); private ArrayList<String> messagesReceivedArray = new ArrayList<>(); //Text boxes to add and display our messages private EditText txtBoxAddMessage; private TextView txtReceivedMessages; private TextView txtMessagesToSend; public void addMessage(View view) { String newMessage = txtBoxAddMessage.getText().toString(); messagesToSendArray.add(newMessage); txtBoxAddMessage.setText(null); updateTextViews(); Toast.makeText(this, "Added Message", Toast.LENGTH_LONG).show(); } private void updateTextViews() { txtMessagesToSend.setText("Messages To Send:\n"); //Populate Our list of messages we want to send if(messagesToSendArray.size() > 0) { for (int i = 0; i < messagesToSendArray.size(); i++) { txtMessagesToSend.append(messagesToSendArray.get(i)); txtMessagesToSend.append("\n"); } } txtReceivedMessages.setText("Messages Received:\n"); //Populate our list of messages we have received if (messagesReceivedArray.size() > 0) { for (int i = 0; i < messagesReceivedArray.size(); i++) { txtReceivedMessages.append(messagesReceivedArray.get(i)); txtReceivedMessages.append("\n"); } } } //Save our Array Lists of Messages for if the user navigates away @Override public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putStringArrayList("messagesToSend", messagesToSendArray); savedInstanceState.putStringArrayList("lastMessagesReceived",messagesReceivedArray); } //Load our Array Lists of Messages for when the user navigates back @Override public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); messagesToSendArray = savedInstanceState.getStringArrayList("messagesToSend"); messagesReceivedArray = savedInstanceState.getStringArrayList("lastMessagesReceived"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); txtBoxAddMessage = (EditText) findViewById(R.id.txtBoxAddMessage); txtMessagesToSend = (TextView) findViewById(R.id.txtMessageToSend); txtReceivedMessages = (TextView) findViewById(R.id.txtMessagesReceived); Button btnAddMessage = (Button) findViewById(R.id.buttonAddMessage); btnAddMessage.setText("Add Message"); updateTextViews(); } } 

Проверка поддержки NFC

В методе MainActivity.java onCreate() добавьте следующее для обработки, когда NFC не поддерживается на устройстве:

 //Check if NFC is available on device mNfcAdapter = NfcAdapter.getDefaultAdapter(this); if(mNfcAdapter != null) { //Handle some NFC initialization here } else { Toast.makeText(this, "NFC not available on this device", Toast.LENGTH_SHORT).show(); } 

Убедитесь, что создали переменную mNfcAdapter в верхней части определения класса:

 private NfcAdapter mNfcAdapter; 

Создание нашего сообщения

Android предоставляет полезные классы и функции, которые позволяют нам упаковывать наши данные. Чтобы соответствовать NDEF, мы можем создать NdefMessage которые содержат один или несколько NdefRecord .

Чтобы отправить сообщение, мы должны сначала его создать. Есть два основных способа справиться с этим:

  1. Вызовите setNdefPushMessage() в классе NfcAdapter . Это примет сообщение NdefMessage отправленное при обнаружении другого устройства с поддержкой NFC.
  2. Переопределите обратные вызовы, чтобы наше NdefMessage было создано только тогда, когда его нужно отправить.

Номер один является предпочтительным методом, если данные не изменятся. Наши данные будут меняться, поэтому мы будем использовать второй вариант.

Для обработки отправки сообщений нам нужно указать обратные вызовы для ответа на события NFC. Поскольку мы будем обновлять наши данные, мы хотим, чтобы сообщение создавалось только тогда, когда оно должно быть отправлено.

Обновите активность следующим образом:

 public class MainActivity extends Activity implements NfcAdapter.OnNdefPushCompleteCallback, NfcAdapter.CreateNdefMessageCallback 

Переопределите соответствующие функции:

 @Override public void onNdefPushComplete(NfcEvent event) { //This is called when the system detects that our NdefMessage was //Successfully sent. messagesToSendArray.clear(); } @Override public NdefMessage createNdefMessage(NfcEvent event) { //This will be called when another NFC capable device is detected. if (messagesToSendArray.size() == 0) { return null; } //We'll write the createRecords() method in just a moment NdefRecord[] recordsToAttach = createRecords(); //When creating an NdefMessage we need to provide an NdefRecord[] return new NdefMessage(recordsToAttach); } 

Теперь убедитесь, что вы указали эти обратные вызовы в методе onCreate :

 //Check if NFC is available on device mNfcAdapter = NfcAdapter.getDefaultAdapter(this); if(mNfcAdapter != null) { //This will refer back to createNdefMessage for what it will send mNfcAdapter.setNdefPushMessageCallback(this, this); //This will be called if the message is sent successfully mNfcAdapter.setOnNdefPushCompleteCallback(this, this); } 

Создание записей

В классе NdefRecord есть несколько служебных функций, которые могут возвращать правильно отформатированный NdefRecord , но чтобы понять концепцию и дать нам больше гибкости, мы собираемся сначала вручную создать NdefRecord .

В NDEF запись состоит из четырех частей:

  1. short которое указывает имя типа нашей полезной нагрузки из списка констант.
  2. byte[] переменной длины byte[] который дает более подробную информацию о нашем типе.
  3. byte[] переменной длины используется как уникальный идентификатор. Это не требуется или часто используется.
  4. byte[] переменной длины byte[] который является нашей фактической полезной нагрузкой.

Добавьте эту функцию в MainActivity.java :

 public NdefRecord[] createRecords() { NdefRecord[] records = new NdefRecord[messagesToSendArray.size()]; for (int i = 0; i < messagesToSendArray.size(); i++){ byte[] payload = messagesToSendArray.get(i). getBytes(Charset.forName("UTF-8")); NdefRecord record = new NdefRecord( NdefRecord.TNF_WELL_KNOWN, //Our 3-bit Type name format NdefRecord.RTD_TEXT, //Description of our payload new byte[0], //The optional id for our Record payload); //Our payload for the Record records[i] = record; } return records; } 

Поскольку мы пишем как отправителя, так и получателя, мы можем точно определить, как мы хотим обрабатывать наши данные. Мы можем вызвать NdefRecord.createApplicationRecord чтобы прикрепить специально отформатированный NdefRecord , который сообщит ОС, какое приложение должно обрабатывать данные. Система попытается открыть приложение для обработки данных раньше других.

Неважно, где в NdefRecord[] мы включаем эту запись, если она присутствует везде, где она будет работать. Обязательно измените длину нашего NdefRecord[] чтобы он был больше, чтобы вместить дополнительную запись, и добавьте следующее перед возвратом в функцию createRecords() .

 //Remember to change the size of your array when you instantiate it. records[messagesToSendArray.size()] = NdefRecord.createApplicationRecord(getPackage()); 

Преимущество создания и присоединения Записи приложения Android состоит в том, что если Android не может найти приложение, оно откроет соединение с магазином Google Play и попытается загрузить ваше приложение (если оно существует).

Примечание . Это не делает транзакцию безопасной и не гарантирует, что ваше приложение будет тем, кто ее откроет. Включение записи приложения только дополнительно указывает наше предпочтение ОС. Если другое действие, которое в настоящее время находится на переднем плане, вызывает NfcAdapter.enableForegroundDispatch, оно может перехватить намерение до того, как оно попадет к нам, нет способа предотвратить это, кроме как иметь нашу активность на переднем плане. Тем не менее, это как можно ближе к тому, чтобы гарантировать, что наше приложение обрабатывает эти данные.

Как уже упоминалось, для создания записей обычно предпочтительнее использовать предоставленные служебные функции. Большинство из этих функций были введены в API 16, и мы пишем для 14 или выше. Итак, мы покрываем все базы, давайте включим проверку уровня API и создадим нашу запись предпочтительным способом, если функция доступна для нас. Измените функцию createRecords() следующим образом:

 public NdefRecord[] createRecords() { NdefRecord[] records = new NdefRecord[messagesToSendArray.size() + 1]; //To Create Messages Manually if API is less than if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { for (int i = 0; i < messagesToSendArray.size(); i++){ byte[] payload = messagesToSendArray.get(i). getBytes(Charset.forName("UTF-8")); NdefRecord record = new NdefRecord( NdefRecord.TNF_WELL_KNOWN, //Our 3-bit Type name format NdefRecord.RTD_TEXT, //Description of our payload new byte[0], //The optional id for our Record payload); //Our payload for the Record records[i] = record; } } //Api is high enough that we can use createMime, which is preferred. else { for (int i = 0; i < messagesToSendArray.size(); i++){ byte[] payload = messagesToSendArray.get(i). getBytes(Charset.forName("UTF-8")); NdefRecord record = NdefRecord.createMime("text/plain",payload); records[i] = record; } } records[messagesToSendArray.size()] = NdefRecord.createApplicationRecord(getPackageName()); return records; } 

Обработка сообщения

Полученное намерение будет содержать массив NdefMessage[] . Поскольку мы знаем длину, ее легко обрабатывать.

 private void handleNfcIntent(Intent NfcIntent) { if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(NfcIntent.getAction())) { Parcelable[] receivedArray = NfcIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); if(receivedArray != null) { messagesReceivedArray.clear(); NdefMessage receivedMessage = (NdefMessage) receivedArray[0]; NdefRecord[] attachedRecords = receivedMessage.getRecords(); for (NdefRecord record:attachedRecords) { String string = new String(record.getPayload()); //Make sure we don't pass along our AAR (Android Application Record) if (string.equals(getPackageName())) { continue; } messagesReceivedArray.add(string); } Toast.makeText(this, "Received " + messagesReceivedArray.size() + " Messages", Toast.LENGTH_LONG).show(); updateTextViews(); } else { Toast.makeText(this, "Received Blank Parcel", Toast.LENGTH_LONG).show(); } } } @Override public void onNewIntent(Intent intent) { handleNfcIntent(intent); } 

Мы переопределяем onNewIntent чтобы мы могли получать и обрабатывать сообщение, не создавая новое действие. Это не обязательно, но поможет заставить все чувствовать себя жидким. Добавьте вызов handleNfcIntent в функции onCreate() и onResume() чтобы убедиться, что все случаи обрабатываются.

 @Override public void onResume() { super.onResume(); updateTextViews(); handleNfcIntent(getIntent()); } 

Это оно! У вас должен быть простой функционирующий мессенджер NFC. Прикрепить файлы разных типов так же просто, как указать другой тип MIME и прикрепить двоичный файл к файлу, который вы хотите отправить. Полный список поддерживаемых типов и их удобных конструкторов смотрите в классах NdefMessage и NdefRecord в документации Android. В Android с NFC доступны более сложные функции, такие как эмуляция тега NFC, чтобы мы могли пассивно читать, но это не просто приложение для обмена сообщениями.