NFC앱 만들기 – 안드로이드 자바
NFC를 간단하게 읽기 또는 입력할 수 있는 안드로이드 앱(자바)을 샘플로 만들어 보겠습니다.
안드로이드 자바 환경에서 만들 앱은 액티비티(Activity)가 아닌 프래그먼트(Fragment)에서 읽고, 쓰기 기능을 구현하도록 하였습니다.
우선, 읽기 쓰기가 가능한 Ntag 213 태그와 안드로이드 스마트폰 및 안드로이드 스튜디오 프로그램을 준비해 주시면 되겠습니다. (시뮬레이터에서는 NFC 테스트가 불가하니 실제 스마트폰이 필요합니다.)
아주 기본적인 기능만 구현하는 샘플 앱으로 읽은 데이터를 서버로 전송하는 등 다양한 방법으로 응용할 수 있을 것으로 생각됩니다.

프로젝트 및 기본 설정
프로젝트 설정
- 기본 프로젝트 설정은 안드로이드 스튜디오를 실행하고 새 프로젝트를 생성합니다.
- 패키지 이름과 프로젝트 이름을 설정합니다.(필자는 “nfc_read_write_test.co” 로 하였습니다.)
- 템플릿은 “Empty Activity” 를 선택합니다.
- build.gradle(Module)에서 compileSdk 및 targetSdk를 34로 설정하고 dependencise에 implementation(“com.google.guava:guava:28.1-android”)를 추가합니다.
- 1개의 Activity와 2개의 Fragment로 구성하였습니다.
AndroidManifest.xml 수정
- 프로젝트 생성 후 AndroidManifest.xml 수정합니다.
- <uses-permission android:name=”android.permission.NFC” /> 를 추가하여 NFC 권한을 요청합니다.
- MainActivity에 <intent-filer>를 추가하여, 앱이 NFC 태그를 감지할 수 있도록 합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.NFC" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
메인액티비트(MainActivity)
메인 액티비티 UI 구성은 Fragment를 구현하는 프레임과 하단에 프래그먼트로 이동할 수 있는 버튼 두개로 구성하였습니다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!--Frame-->
<FrameLayout
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="0dp"
android:layout_marginBottom="80dp"/>
<!--하단버튼들-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:padding="15dp"
android:gravity="center_vertical"
>
<Button
android:id="@+id/btn_fragment1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="3"
android:drawableTop="@drawable/ic_nfc"
android:drawablePadding="3dp"
android:text="태그 읽기"
android:textSize="13dp"
android:textColor="@color/black"
android:background="@color/transparent"
/>
<Button
android:id="@+id/btn_fragment2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="3"
android:drawableTop="@drawable/ic_input"
android:drawablePadding="3dp"
android:text="태그쓰기"
android:textSize="13dp"
android:textColor="@color/black"
android:background="@color/transparent"
/>
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
메인액티비티에서는 MoveFragment1()은 읽기 탭으로 MoveFragment2()은 쓰기 탭으로 이동하게 하였으며, NFC 이벤트를 각각의 Fragment로 전달 할 수 있게 하였습니다.
MainActivity.java
package nfc_read_write_test.co;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.widget.Button;
import nfc_read_write_test.co.fragments.Fragment_ReadNFC;
import nfc_read_write_test.co.fragments.Fragment_WriteNFC;
public class MainActivity extends AppCompatActivity {
Button btn_fragment1, btn_fragment2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MoveFragment1(); //기본 프래그먼트
//Fragment로 이동하기
btn_fragment1 = findViewById(R.id.btn_fragment1);
btn_fragment2 = findViewById(R.id.btn_fragment2);
btn_fragment1.setOnClickListener(view -> {
MoveFragment1();
});
btn_fragment2.setOnClickListener(view -> {
MoveFragment2();
});
}
////////////////////////////////// NFC 관련 //////////////////////////////////
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// NFC 이벤트를 Fragment로 전달
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.frame);
if (fragment instanceof Fragment_ReadNFC) {
((Fragment_ReadNFC) fragment).handleNfcIntent(intent);
}
else if (fragment instanceof Fragment_WriteNFC) {
((Fragment_WriteNFC) fragment).handleNfcIntent2(intent);
}
}
@Override
protected void onPause() {
super.onPause();
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
nfcAdapter.disableForegroundDispatch(this);
}
////////////////////////////////// NFC 관련 끝 //////////////////////////////////
//Move Fragments Start
public void MoveFragment1(){
//1번 프래그먼트로 이동
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment_ReadNFC fragment1 = new Fragment_ReadNFC();
transaction.replace(R.id.frame, fragment1);
transaction.commit();
}
public void MoveFragment2(){
//2번 프래그먼트로 이동
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment_WriteNFC fragment2 = new Fragment_WriteNFC();
transaction.replace(R.id.frame, fragment2);
transaction.commit();
}
}

읽기 프래그먼트(Fragment_ReadNFC)
읽기 프래그먼트 UI는 프래그먼트 이름, 태그 UID, 태그 내용을 보여주는 3개의 TextView로 구성하였습니다.
fragment_read_nfc.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fragment_ReadNFC"
/>
<TextView
android:id="@+id/txt_nfcUID"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TAG UID"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_marginTop="50dp"
/>
<TextView
android:id="@+id/txt_str"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="STR"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_marginTop="50dp"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment_ReadNFC에서는 NFC 핸들링을 위해 NfcAdapter를 초기화하고 NFC 액션을 처리합니다.
handleNfcIntent 메서드를 재정의하여 NFC 태그를 처리 및 읽은 후 NFC Tag UID과 Tag 내용을 읽습니다.
readNFCuid에서는 Tag UID(고유번호)를 표시하고, processNdefMessage에서는 Tag 내용을 표시합니다.
※ 샘플 앱에서는 읽고 쓰는데 사용하는 record를 1개만 사용하였습니다. 필요에 따라 record를 추가할 수 있습니다.
Fragment_ReadNFC.java
package nfc_read_write_test.co.fragments;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.List;
import nfc_read_write_test.co.R;
import nfc_read_write_test.co.utill.NdefMessageParser;
import nfc_read_write_test.co.utill.ParsedRecord;
import nfc_read_write_test.co.utill.TextRecord;
import nfc_read_write_test.co.utill.UriRecord;
public class Fragment_ReadNFC extends Fragment {
private NfcAdapter nfcAdapter;
PendingIntent pendingIntent;
private TextView str_UID, Str;
private String recordStr;
@SuppressLint("MissingInflatedId")
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//view설정
View rootView = inflater.inflate(R.layout.fragment_read_nfc, container, false);
str_UID = rootView.findViewById(R.id.txt_nfcUID);
Str = rootView.findViewById(R.id.txt_str);
return rootView;
}
////////////////////////////////// NFC 관련 //////////////////////////////////
@Override
public void onPause() {
super.onPause();
// Foreground dispatch 비활성화
nfcAdapter.disableForegroundDispatch(requireActivity());
}
@Override
public void onResume() {
super.onResume();
// NFC 어댑터 초기화
nfcAdapter = NfcAdapter.getDefaultAdapter(requireContext());
if (nfcAdapter == null) {
// 기기가 NFC를 지원하지 않을 경우 처리
Toast.makeText(requireContext(), "NFC를 지원하지 않는 기기입니다.", Toast.LENGTH_SHORT).show();
return;
}
if (!nfcAdapter.isEnabled()) {
// NFC가 비활성화되어 있는 경우 처리
Toast.makeText(requireContext(), "NFC가 비활성화되어 있습니다.", Toast.LENGTH_SHORT).show();
}
// NFC 태그 감지 필터 설정
IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
IntentFilter[] filters = new IntentFilter[]{tagDetected};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pendingIntent = PendingIntent.getActivity(
requireContext(), 0,
new Intent(requireContext(), requireContext().getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_MUTABLE //
);
} else {
pendingIntent = PendingIntent.getActivity(
requireContext(), 0,
new Intent(requireContext(), requireContext().getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0
);
}
nfcAdapter.enableForegroundDispatch(requireActivity(), pendingIntent, filters, null);
}
public static final String CHARS = "0123456789ABCDEF";
public static String toHexString(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte datum : data) {
sb.append(CHARS.charAt((datum >> 4) & 0x0F))
.append(CHARS.charAt(datum & 0x0F));
}
return sb.toString();
}
public void handleNfcIntent(Intent intent) {
String action = intent.getAction();
NdefMessage[] msgs;
if(NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) || NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
//Get Tag ID
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String nfcUID = null;
if (tag != null) {
byte[] tagId = tag.getId();
nfcUID = toHexString(tagId);
}
readNFCuid( nfcUID);
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMsgs != null) {
msgs = new NdefMessage[rawMsgs.length];
for (int i = 0; i < rawMsgs.length; i++) {
msgs[i] = (NdefMessage) rawMsgs[i];
processNdefMessage(msgs[i]);
}
}
}
}
private void readNFCuid(String nfcUID) {
if (nfcUID != null) {
str_UID.setText("태그UID: "+nfcUID);
} else {
Toast.makeText(this.requireContext(), "NFC 태그 UID 읽는데 실패하였습니다. 다시 시도하시거나 다른 태그를 사용하세요!", Toast.LENGTH_SHORT).show();
}
}
private void processNdefMessage(NdefMessage msgs) {
if (msgs != null) {
List<ParsedRecord> records = NdefMessageParser.parse(msgs);
final int size = records.size();
if(size>0){
ParsedRecord record0 = records.get(0);
int recordType0 = record0.getType();
if (recordType0 == ParsedRecord.TYPE_TEXT) {
recordStr = ((TextRecord) record0).getText() ;
} else if (recordType0 == ParsedRecord.TYPE_URI) {
recordStr = ((UriRecord) record0).getUri().toString() ;
}
Str.setText("태그내용: "+ recordStr);
} else{
Toast.makeText(this.requireContext(), "태그 내용이 없습니다.", Toast.LENGTH_SHORT).show();
}
}
}
////////////////////////////////// NFC 관련 끝 //////////////////////////////////
}

쓰기 프래그먼트(Fragment_WriteNFC)
쓰기 프래그먼트 UI는 프래그먼트명을 표시하는 1개 TextView와 태그에 내용을 입력할 1개 EditText로 구성하였습니다.
fragment_write_nfc.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Fragment_WrtieNFC"
/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="25dp"
app:endIconMode="clear_text"
app:hintEnabled="false"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/writeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_round1"
android:hint="내용 입력후 NFC 태그에 태깅"
android:textSize="13dp"
android:textStyle="bold"
android:padding="15dp"
android:inputType="textMultiLine"
android:lines="5"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment_WriteNFC에서는 읽기 프래그먼트와 마찬가지로 NFC 어댑터를 초기화하고 NFC 액션을 처리합니다.
processTag()에서 입력된 값을 writeTag()에 보내서 NFC 태그에 내용을 입력합니다. 정상적으로 입력되면 Toast로 알림이 뜨고 앱이 종료됩니다.
※ 입력 과정과 마찬가지로 쓰기 과정에서도 입력하는 record를 1개만 설정했습니다. 필요에 따라 태그 용량에 맞게 추가할 수 있을 것입니다.
package nfc_read_write_test.co.fragments;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.nfc.tech.NdefFormatable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.io.IOException;
import nfc_read_write_test.co.R;
public class Fragment_WriteNFC extends Fragment {
private NfcAdapter nfcAdapter;
PendingIntent pendingIntent;
EditText writeText;
@SuppressLint("MissingInflatedId")
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//view설정
View rootView = inflater.inflate(R.layout.fragment_write_nfc, container, false);
writeText = rootView.findViewById(R.id.writeText);
return rootView;
}
////////////////////////////////// NFC 관련 //////////////////////////////////
@Override
public void onPause() {
super.onPause();
// Foreground dispatch 비활성화
nfcAdapter.disableForegroundDispatch(requireActivity());
}
@Override
public void onResume() {
super.onResume();
// NFC 어댑터 초기화
nfcAdapter = NfcAdapter.getDefaultAdapter(requireContext());
if (nfcAdapter == null) {
// 기기가 NFC를 지원하지 않을 경우 처리
Toast.makeText(requireContext(), "NFC를 지원하지 않는 기기입니다.", Toast.LENGTH_SHORT).show();
return;
}
if (!nfcAdapter.isEnabled()) {
// NFC가 비활성화되어 있는 경우 처리
Toast.makeText(requireContext(), "NFC가 비활성화되어 있습니다.", Toast.LENGTH_SHORT).show();
}
// NFC 태그 감지 필터 설정
IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
IntentFilter[] filters = new IntentFilter[]{tagDetected};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pendingIntent = PendingIntent.getActivity(
requireContext(), 0,
new Intent(requireContext(), requireContext().getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_MUTABLE
);
} else {
pendingIntent = PendingIntent.getActivity(
requireContext(), 0,
new Intent(requireContext(), requireContext().getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0
);
}
nfcAdapter.enableForegroundDispatch(requireActivity(), pendingIntent, filters, null);
}
public void handleNfcIntent2(Intent intent) {
if (intent != null) {
processTag(intent); // processTag 메소드 호출
}
}
// handleNfcIntent2 메소드 수행 후 호출되는 메소드
private void processTag(Intent intent) {
// EditText에 입력된 값을 가져옴
String s1 = writeText.getText().toString();
NdefRecord s= NdefRecord.createUri(s1); //메시지
// 감지된 태그를 가리키는 객체
Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
// 아무것도 입력받지 않으면 태그에 쓰지 않음
if (s.toString().equals("")) {
Toast.makeText(requireActivity(), "내용을 입력해주세요!", Toast.LENGTH_LONG).show();
}
// 입력받은 값을 감지된 태그에 씀
else {
NdefMessage message = createTagMessage(s);
writeTag(message, detectedTag);
}
}
// 감지된 태그에 NdefMessage를 쓰는 메소드
public 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.makeText(requireActivity(), "This Tag is Read-Only", Toast.LENGTH_LONG).show(); //buring mode
return false;
}
if (ndef.getMaxSize() < size) {
Toast.makeText(requireActivity(), "입력 실패", Toast.LENGTH_SHORT).show();
return false;
}
ndef.writeNdefMessage(message);
Toast.makeText(requireActivity(), "입력완료", Toast.LENGTH_LONG).show();
requireActivity().finish(); //앱 종료
} else {
Toast.makeText(requireActivity(), "format is needed for encoding", Toast.LENGTH_SHORT).show();
NdefFormatable formatable = NdefFormatable.get(tag);
if (formatable != null) {
try {
formatable.connect();
formatable.format(message);
} catch (IOException ex) {
ex.printStackTrace();
}
}
return false;
}
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
return true;
}
private NdefMessage createTagMessage(NdefRecord s) { //입력수에 맞게 늘림
NdefRecord[] records = new NdefRecord[1];
records[0] = s; //입력수에 맞게 늘림
return new NdefMessage(records);
}
}

정말 감사합니다!!