안드로이드 NFC

이영준·2023년 4월 25일
0

📌 NFC란?

  • 가까운 거리에서 동작하는 근거리 무선 통신 기술
  • 보통 1-10cm 이내에서 데이터를 주고받음
  • RFID 기술 + 스마트카드 기술의 장점을 접목
  • 양방향 실시간 데이터 통신
  • 최대 848Kbps의 데이터 전송 가능
  • 블루투스보다 전송속도는 느리나 통신설정 시간이 짧아 인식과 반응속도가 빠름

📌 NFC 운용모드 및 구성

3가지 모드를 지원한다

  • 태그 읽기 / 쓰기
  • p2p 통신
  • HCE(Host Card Emulation) 모드

NFC 태그는 여러개의 NdefMessage로 구성된다.
NDEF : NFC Formum Data Exchange Format의 약자로 NFC Tag에 NFC data를 저장하기 위한 컨테이너 포맷

  • 정의딘 여러 타입이 있다
    : URI, TextRecord, AAR
    NdefMessage는 여러 개의 NdefRecord로 구성된다.

    📌 NFC 태그 인식절차

태그 정보를 인텐트에 데이터와 식별 정보로 담아 전달한다.
3가지로 식별하는데

  • ACTION_NDEF_DISCOVERED
  • ACTION_TECH_DISCOVERED
  • ACTION_TAG_DISCOVERED
    중 하나이다.
    NDEF로 인식하면 NDEF, TECH로 인식하면 TECH, 이도저도 아니면 TAG이다.

📌 NFC 태그 구현

  • manifest 설정
<uses-permission android:name="android.permission.NFC" />
  • activity에서 NDEF 액션 intent 설정
<intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="text/plain"/>
            </intent-filter>

nfc 태그가 인식되면 백그라운드에서도 해당 액티비티로 바로 이동하도록 했다. text타입의 nfc 데이터를 읽을 수 있도록 하였다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val action = intent.action
        binding.textview.text = action
        Toast.makeText(this, "onCreate$action", Toast.LENGTH_SHORT).show()
        Log.d(TAG, "onCreate: $action")

        if(action == NfcAdapter.ACTION_NDEF_DISCOVERED){
            val data = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
            val message = data?.get(0) as NdefMessage
            val record = message.records[0] as NdefRecord
            val byteArr = record.payload

            Log.d(TAG, "onCreate: ${String(byteArr)}")
        }
    }

NFC를 통해 오는 데이터 형식이 뭔지 모르니 가장 범용적인

getParcelableArrayExtra로 받아와서 비트단위 배열을 다시 내가 받을 타입으로 바꿔준다.

📌 NFC Manager

시스템으로 받아오는 NFC를 관리하는 manager
NFCAdapter를 얻을 때 사용하는 클래스

📌 NFC Adapter

NFC 하드웨어에 접근하는 기능을 제공하는 클래스
getDefaultAdapter 함수를 통해 얻어옴

  • 시스템으로부터 NFC Manager를 얻은 후에 manager로부터 기본 adapter를 가져올 때 사용

    • nAdapter = NfcAdapter.getDefaultAdapter(this)
  • 주요함수

    • enableForeroundDispatch()/disableForegroundDispatch() : 포그라운드기능 활성화, 비활성화

📌 NDEF Message , NDEF Record

위 예제 코드에서 알 수 있는데,
NFC 태그 안에는 NDEF message들로 이뤄져 있고, 그 message는 NDEF Record로 이루어져 있다.

NDEF Message를 intent에서 getParcelableArrayExtra로 가져온다.
NDEF REcord는 getPayload로 안에 포함된 어플리케이션 데이터 정보를 얻는다.
이들로 NDEF를 구성하는데,
NDEF 주요 메서드로는
connect, writeNdefMessage, getMaxSize, makeReadOnly, canMakeReadOnly, isWritable 등이 있다.

NDEF 상위 개념인 TAG가 지원하는 함수는
getTechList, getId

📌 Intent로 정보 획득

예제 코드와 같이 필요한 정보를 가져오기 위한 action 이름을 써서 인텐트에서 값을 받아온다.
대표적을
NfcAdapter.EXTRA_NDEF_MESSAGE : Tag에 저장된 NDEF Message 조회시
가 있다.

    fun processIntent(intent: Intent){
        if(intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED){

            //태그의 ID 정보 조회
            val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
            Log.d(TAG, "processIntent: ${tag?.id}")

            //태그 안의 데이터의 정보
            val data = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
            val message = data!![0] as NdefMessage
            val record = message.records[0]

            val byteArr = record.payload
            Log.d(TAG, "processIntent: ${String(byteArr)}")
            binding.textview.text = String(byteArr)
        }
    }

📌 NFC 포그라운드 모드

NFC 앱을 스마트폰에서 사용하다 보면 태그 데이터를 다른 NFC 앱보다 가장 먼저 처리하고 싶을 수 있다. 더 나아가 시스템보다 먼저 NFC 요청을 처리하고 싶을 수 있다. 즉, intent-filter를 scan 하기 전에 동작하도록 하는 것을 foreground 모드로 해결할 수 있다.

Foreground 모드에서는 pending intent를 만든다.
그리고,
nfc adapter를 nAdapter.enableForegroundDispatch(this, pIntent, filters, null) enableForegroundDispatch 해주어 포그라운드 동작을 하도록 한다.

class MainActivity : AppCompatActivity() {
    private lateinit var nAdapter: NfcAdapter
    private lateinit var pIntent: PendingIntent
    private lateinit var filters: Array<IntentFilter>
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val action = intent.action
        binding.textview.text = action
        Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show()

        nAdapter = NfcAdapter.getDefaultAdapter(this)
        val i = Intent(this, MainActivity::class.java) //자기자신으로 다시 호출. MainActivity::class.java == javaClass
        i.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
        // 당장 실행하지 않으므로 pending intent 처리
        pIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_MUTABLE)

        //수신할 태그 데이터 관련 필터 생성
        //Text Record 수신하기 위한 필터
        val ndef_filter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
//        ndef_filter.addDataType("text/plain")
        ndef_filter.addDataScheme("https")

        filters = arrayOf(ndef_filter)
    }

    override fun onResume() {
        super.onResume()
        nAdapter.enableForegroundDispatch(this, pIntent, filters, null)
    }

    override fun onPause() {
        super.onPause()
        nAdapter.disableForegroundDispatch(this)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        Toast.makeText(this, "onNewIntent", Toast.LENGTH_SHORT).show()
        val action = intent.action
        Log.d(TAG, "New Intent action : $action")
        parseData(intent)
    }

    private fun parseData(intent: Intent) {
        if(intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED){

            //태그의 ID 정보 조회
            val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
            Log.d(TAG, "processIntent: ${tag?.id}")

            //태그 안의 데이터의 정보
            val data = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
            val message = data!![0] as NdefMessage
            val record = message.records[0]

            val byteArr = record.payload
            Log.d(TAG, "processIntent: ${String(byteArr)}")
            binding.textview.text = String(byteArr)
        }

    }

}

ndef_filter.addDataScheme("https")와 같은 필터로 읽히지 않는 nfc태그라면 포그라운드 모드로 내가 호출되는 것이 아닌 nfc를 키는 앱을 고르는 chooser가 나타난다.

intent flag값이 singleTop으로 설정된 상태에서 인텐트를 수신하려면 onNewIntent함수를 오버라이딩 해야 한다.

참고로 여러개의 필터 조건을 넣고 싶다면 각각 마다 intent filter를 만들어줘야 한다.

        val ndef_filter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
//        ndef_filter.addDataType("text/plain")
        ndef_filter.addDataScheme("https")

        //둘다 쓰려면
        val ndef_filter1 = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
        ndef_filter1.addDataType("text/plain")

        filters = arrayOf(ndef_filter , ndef_filter1)

모든 태그를 포그라운드에서 감지하려면 필터를

val ndef_filter = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
        filters = arrayOf(ndef_filter)

해주면 될 것이다.

📌 onNewIntent의 갱신된 intent를 oncreate, onresume 등에 전달

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        // 이후 다른 method에서 getIntent() 호출시 newIntent에서 받은 Intent를 사용하기 위함.
        // set하지 않으면 기존의 intent인 action Main이 나옴.
        setIntent(intent)
        getNFCData(getIntent())
    }

onNewIntent의 intent는 onresume, oncreate의 intent와 별개의 intent로 불릴 때마다 생성된다. 이를 라이프사이클에 전달해주기 위해선
setIntent(intent)를 해주면 된다.

📌 NFC에 내용 쓰기

다양한 유형의 정보를 NFC 태그에 쓸 수 있다.

🔑 AAR

AAR은 태그에 실행할 앱을 명시적으로 지정할 수 있다.
이 안에는 앱을 실행시킬 애플리케이션의 패키지명이 저장된다. 태그 내 AAR이 포함되어 있는 경우 Tag Dispatch System의 처리 순서는
어플리케이션 실행 -> 없으면 구글 플레이스토어 순이다.

var ndefR: NdefRecord? = null
...
 ndefR1 = NdefRecord.createApplicationRecord("com.ssafy.android.tag_recognition")

record에 위처럼 createApplicationRecord로 패키지명을 넣어주면 해당 패키지에 해당되는 앱이 실행된다.

🔑 전체 NFC writing 코드

private lateinit var nfcAdapter: NfcAdapter
    private lateinit var pIntent: PendingIntent
    private lateinit var filters: Array<IntentFilter>
    private lateinit var tagType: String
    private lateinit var tagData: String

    private lateinit var binding: ActivityTagWriteBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityTagWriteBinding.inflate(layoutInflater)
        setContentView(binding.root)

        nfcAdapter = NfcAdapter.getDefaultAdapter(this)
        if (nfcAdapter == null) {
            finish()
        }

        //넘어온 데이터를 변수에 저장한다.
        tagType = intent.getStringExtra("type").toString()
        tagData = intent.getStringExtra("data").toString()
        Toast.makeText(this, "type : $tagType, data : $tagData", Toast.LENGTH_SHORT).show()

        //태그 정보가 포함된 인텐트를 처리할 액티비티 지정
        val intent = Intent(this, TagWriteActivity::class.java)

        //SingleTop설정
        intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
        pIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)

        val tagFilter = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
        filters = arrayOf(tagFilter)
    }

    public override fun onResume() {
        super.onResume()
        nfcAdapter.enableForegroundDispatch(this, pIntent, filters, null)
    }

    public override fun onPause() {
        super.onPause()
        nfcAdapter.disableForegroundDispatch(this)
    }

    public override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        //태그에 데이터를 write 작업을 수행해야 한다..
        val action = intent.action
        if (action == NfcAdapter.ACTION_NDEF_DISCOVERED || action == NfcAdapter.ACTION_TECH_DISCOVERED || action == NfcAdapter.ACTION_TAG_DISCOVERED) {
            //Log.d(TAG, "ACTION_NDEF_DISCOVERED...")
            //1. 태그 detect.... Tag 객체
            val detectTag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
            //writeTag 함수 호출
            writeTag(makeNdefMessage(tagType, tagData), detectTag)
        }
    }

    //T, "SSAFY"  / U, "https://m.naver.com"
    private fun makeNdefMessage(type: String?, data: String?): NdefMessage {
        var ndefR: NdefRecord? = null
        var ndefR1: NdefRecord? = null

        if (type == "T") {//TextRecord
            ndefR = NdefRecord.createTextRecord("en", data)
            //AAR
            ndefR1 = NdefRecord.createApplicationRecord("com.ssafy.android.tag_recognition")
        } else if (type == "U") {//URI
            ndefR = NdefRecord.createUri(data)
        } else {
            //다른 형태....
        }
        return NdefMessage(arrayOf(ndefR,ndefR1))
    }

    //NFC tag 에 데이터를 write 코드...
    private fun writeTag(msg: NdefMessage, tag: Tag?) {

        //Ndef 객체를 얻는다 : Ndef.get(tag)
        val ndef = Ndef.get(tag)
        val msgSize = msg.toByteArray().size
        try {
            if (ndef != null) {

                //ndef 객체를 이용해서 connect
                ndef.connect()
                //tag가 write모드를 지원하는지 여부 체크
                if (!ndef.isWritable) {
                    Toast.makeText(this, "Write를 지원하지 않습니다..", Toast.LENGTH_SHORT).show()
                    return
                }
                if (ndef.maxSize < msgSize) {
                    Toast.makeText(this, "Write할 데이터가 태그 크기보다 큽니다..", Toast.LENGTH_SHORT).show()
                    return
                }

                //ndef객체의 writeNdefMessage(msg) 태그에 write 한다...
                ndef.writeNdefMessage(msg)
                Toast.makeText(this, "태그에 데이터를 write 하였습니다..", Toast.LENGTH_SHORT).show()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            Toast.makeText(this, "Failed to write tag", Toast.LENGTH_SHORT).show()
        }
    }
}
profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글