NFC앱 만들기 (안드로이드 자바)

NFC앱 만들기 – 안드로이드 자바

NFC를 간단하게 읽기 또는 입력할 수 있는 안드로이드 앱(자바)을 샘플로 만들어 보겠습니다.

안드로이드 자바 환경에서 만들 앱은 액티비티(Activity)가 아닌 프래그먼트(Fragment)에서 읽고, 쓰기 기능을 구현하도록 하였습니다.

우선, 읽기 쓰기가 가능한 Ntag 213 태그와 안드로이드 스마트폰 및 안드로이드 스튜디오 프로그램을 준비해 주시면 되겠습니다. (시뮬레이터에서는 NFC 테스트가 불가하니 실제 스마트폰이 필요합니다.)

아주 기본적인 기능만 구현하는 샘플 앱으로 읽은 데이터를 서버로 전송하는 등 다양한 방법으로 응용할 수 있을 것으로 생각됩니다.

nfc앱만들기
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);
    }

}
태그 쓰기

1 thought on “NFC앱 만들기 (안드로이드 자바)”

Leave a Comment