NFC 태그가 앱에 의해 감지될 때android.nfc.action.TAG_DISCOVERED는 NFC 태그가 감지될 때 인텐트를 전달하는 데 필요하다. 이를 위해AndroidManifest.xml에 설정을 추가해야 한다. 이 설정은TAG_DISCOVERED,TECH_DISCOVERED,NDEF_DISCOVERED와 같은 액션을 통해 NFC 태그 감지 시 앱으로 인텐트를 전달하는 역할을 한다.
권한 설명
android.nfc.action.NDEF_DISCOVERED: NDEF 메시지를 포함한 NFC 태그가 감지되었을 때 작동하는 액션이다. 주로 특정 애플리케이션 데이터 또는 MIME 타입이 있는 태그에서 사용된다.
android.nfc.action.TAG_DISCOVERED: NFC 태그가 감지되었을 때 작동하는 기본 액션으로, NDEF 메시지가 포함되지 않은 모든 NFC 태그를 처리할 수 있다.
android.nfc.action.TECH_DISCOVERED: 태그가 특정 기술(예: NfcA, MifareUltralight 등)을 지원할 때 작동한다. 이를 통해 특정 기술 스택을 가진 태그를 처리할 수 있다. 예를 들어nfc_tech_filter.xml에서 정의된 기술과 일치하는 태그가 감지되었을 때 인텐트를 처리한다.
추가 설명
MIME 타입은 NDEF 태그의 데이터를 처리할 때 필수입니다. 예를 들어,"text/plain"은 특정 애플리케이션 데이터 타입을 지정한 것입니다.
NDEF_DISCOVERED는 NDEF 메시지가 포함된 태그만 처리할 수 있으므로, 태그의 내용이 특정 유형(MIME 타입)일 때만 작동하게끔 설정됩니다.
TECH_DISCOVERED는 기술 스택을 기반으로 태그를 처리하며,nfc_tech_filter.xml에서 정의한 기술과 일치하는 태그가 감지되면 작동합니다.
AndroidManifest.xml 에 추가된 설정 예 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mwkg.testwebview">
<!-- NFC 권한 설정 -->
<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:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestWebView">
<!-- FCM 푸시 알림 서비스 등록 -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.TestWebView">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- NFC NDEF 태그 인텐트 필터 -->
<intent-filter>
<!-- NDEF 메시지를 포함한 NFC 태그가 감지될 때 앱으로 인텐트를 전달 -->
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<!-- NDEF 메시지에서 처리할 MIME 유형 지정 (예: 특정 애플리케이션 또는 텍스트 MIME 유형) -->
<data android:mimeType="text/plain" /> <!-- ex: "text/plain" 또는 "application/vnd.*" -->
</intent-filter>
<!-- NFC 태그 인텐트 필터 -->
<intent-filter>
<!-- 모든 NFC 태그가 감지될 때 앱으로 인텐트를 전달 (NDEF 메시지가 없을 때도 작동) -->
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- NFC 기술 인텐트 필터 -->
<intent-filter>
<!-- NFC 태그에 기술 스택이 감지되면 앱으로 인텐트를 전달 (특정 기술 유형을 지원하는 태그를 처리) -->
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- NFC 기술 필터 설정 (nfc_tech_filter.xml 파일을 참조하여 특정 기술을 처리) -->
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>
nfc_tech_filter.xml 사용 예 :
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Ndef 태그 (NFC Data Exchange Format) -->
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
<!-- NdefFormatable 태그 (NFC 태그를 NDEF 형식으로 포맷할 수 있는 경우) -->
<tech-list>
<tech>android.nfc.tech.NdefFormatable</tech>
</tech-list>
<!-- NfcA 태그 (ISO 14443-3A) -->
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
<!-- NfcB 태그 (ISO 14443-3B) -->
<tech-list>
<tech>android.nfc.tech.NfcB</tech>
</tech-list>
<!-- NfcF 태그 (Felica, JIS 6319-4) -->
<tech-list>
<tech>android.nfc.tech.NfcF</tech>
</tech-list>
<!-- NfcV 태그 (ISO 15693) -->
<tech-list>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
<!-- IsoDep 태그 (ISO 14443-4) -->
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
</tech-list>
<!-- MifareClassic 태그 -->
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
<!-- MifareUltralight 태그 -->
<tech-list>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
<!-- 기술 조합이 필요한 경우 -->
<!-- NfcA와 Ndef, MifareUltralight를 동시에 지원하는 태그 -->
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
</resources>
NdefFormatable은NFC 태그를 NDEF 형식으로 포맷할 수 있을 때 사용하는 인터페이스이다. 만약 NFC 태그가 NDEF 형식으로 포맷되지 않은 경우,NDEF 포맷팅작업을 수행할 수 있다. 이 기능은 주로 NFC 태그가기본적으로 NDEF 메시지를 지원하지 않는 경우에 사용된다. NDEF 포맷팅은 태그에 데이터를 기록할 수 있는 형태로 변환하는 과정을 말한다.
NdefFormatable사용 시점:
NFC 태그가 아직 NDEF 포맷으로 되어 있지 않은 경우에NdefFormatable을 사용하여 태그를 포맷하고 NDEF 메시지를 기록할 수 있다.
한 번만 포맷할 수 있는 태그,비어 있는 상태에서만 포맷할 수 있는 태그에 유용하다.
일반적으로NFC 태그를 처음 사용하거나 비어 있는 상태에서 NDEF 메시지를 기록하려는 경우NdefFormatable은 포맷할 수 있는 태그에 대해connect(),format()및writeNdefMessage()와 같은 작업을 제공한다.
다음은NdefFormatable을 사용하여 NFC 태그를 NDEF 형식으로 포맷하고 데이터를 기록하는 예이다.
fun formatTagWithNdefMessage(tag: Tag, ndefMessage: NdefMessage): Boolean {
val ndefFormatable = NdefFormatable.get(tag)
if (ndefFormatable != null) {
try {
// NDEF 포맷팅을 위해 태그 연결
ndefFormatable.connect()
// NDEF 메시지로 태그 포맷 및 데이터 기록
ndefFormatable.format(ndefMessage)
Log.d("NFC", "태그를 NDEF 형식으로 포맷하고 메시지를 기록했습니다.")
return true
} catch (e: IOException) {
Log.e("NFC", "태그 포맷팅 실패: ${e.message}")
} finally {
try {
ndefFormatable.close()
} catch (e: IOException) {
Log.e("NFC", "태그 닫기 실패: ${e.message}")
}
}
} else {
Log.e("NFC", "이 태그는 NDEF 포맷이 지원되지 않습니다.")
}
return false
}
주요 메서드 설명:
NdefFormatable.get(tag): Tag객체에서NdefFormatable인스턴스를 얻는다. NDEF 포맷 미지원 시null을 반환 한다.
connect(): NFC 태그와 연결을 시도한다.
format(NdefMessage): 태그를 NDEF 형식으로 포맷하고, 제공된 NDEF 메시지를 기록한다.
close(): 태그와의 연결 종료한다.
주의사항:
NDEF 형식으로 포맷하면 원래 상태로 되돌릴 수 없다.
일부 태그는 포맷 후에도 더 이상 기록할 수 없는 경우가 있다. (읽기 전용 태그로 변환).
NFC Tag에서 얻을 수 있는 정보를 json 문자열 포맷으로 반환하는 함수를 만들어보았다.
테그 정보 :
UID (고유 ID): tag.id에서추출한고유식별자.
Tech List: 태그가지원하는모든기술목록 (tag.techList).
NfcA (ISO 14443-3A): ATQA, SAK, 최대전송길이, 타임아웃정보.
IsoDep (ISO 14443-4): 역사적바이트(historicalBytes), 고층응답(hiLayerResponse), 최대전송길이, 타임아웃정보.
NfcB (ISO 14443-3B): 응용데이터, 프로토콜정보, 최대전송길이.
NfcF (Felica): 제조사정보, 시스템코드, 최대전송길이, 타임아웃.
NfcV (ISO 15693): 응답플래그, DSFID, 최대전송길이.
MifareClassic: 타입, 크기, 섹터및블록수.
MifareUltralight:타입정보(Ultralight, Ultralight C).
구현 함수는 다음과 같다.
fun readTagDetailsAsJson(tag: Tag): String {
val tagInfo = mutableMapOf<String, Any>()
// UID 정보 (태그 고유 ID)
tagInfo["uid"] = tag.id.joinToString("") { String.format("%02X", it) }
// 기술 스택 나열 (태그가 지원하는 모든 기술들)
tagInfo["techList"] = tag.techList.joinToString()
// NfcA (ISO 14443-3A) 정보
val nfcA = NfcA.get(tag)
if (nfcA != null) {
tagInfo["NfcA"] = mapOf(
// ATQA: Answer to Request Type A. 태그가 리더기에 응답할 때 사용하는 정보
"atqa" to nfcA.atqa.joinToString("") { String.format("%02X", it) },
// SAK: Select Acknowledge. 태그가 리더기에 제공하는 지원 기능 정보
"sak" to String.format("%02X", nfcA.sak),
// 한 번에 보낼 수 있는 최대 전송 길이 (바이트 단위)
"maxTransceiveLength" to nfcA.maxTransceiveLength,
// 통신 타임아웃 시간 (밀리초 단위)
"timeout" to nfcA.timeout
)
}
// IsoDep (ISO 14443-4) 정보
val isoDep = IsoDep.get(tag)
if (isoDep != null) {
tagInfo["IsoDep"] = mapOf(
// Historical Bytes: 태그가 초기 통신 설정 시 리더기에 제공하는 추가 정보
"historicalBytes" to (isoDep.historicalBytes?.joinToString("") { String.format("%02X", it) } ?: "N/A"),
// Higher Layer Response: 고급 프로토콜에 대한 응답 정보
"hiLayerResponse" to (isoDep.hiLayerResponse?.joinToString("") { String.format("%02X", it) } ?: "N/A"),
// 한 번에 전송할 수 있는 최대 데이터 길이 (바이트 단위)
"maxTransceiveLength" to (isoDep.maxTransceiveLength.toString() ?: "N/A"),
// 통신 타임아웃 시간 (밀리초 단위)
"timeout" to isoDep.timeout
)
}
// NfcB (ISO 14443-3B) 정보
val nfcB = NfcB.get(tag)
if (nfcB != null) {
tagInfo["NfcB"] = mapOf(
// Application Data: 태그의 애플리케이션과 관련된 데이터
"applicationData" to (nfcB.applicationData?.joinToString("") { String.format("%02X", it) } ?: "N/A"),
// Protocol Info: 태그가 제공하는 프로토콜 정보
"protocolInfo" to (nfcB.protocolInfo?.joinToString("") { String.format("%02X", it) } ?: "N/A")
)
}
// NfcF (Felica, JIS 6319-4) 정보
val nfcF = NfcF.get(tag)
if (nfcF != null) {
tagInfo["NfcF"] = mapOf(
// 제조사 코드
"manufacturer" to (nfcF.manufacturer?.joinToString("") { String.format("%02X", it) } ?: "N/A"),
// 시스템 코드
"systemCode" to (nfcF.systemCode?.joinToString("") { String.format("%02X", it) } ?: "N/A"),
// 한 번에 전송할 수 있는 최대 데이터 길이 (바이트 단위)
"maxTransceiveLength" to nfcF.maxTransceiveLength.toString(),
// 통신 타임아웃 시간 (밀리초 단위)
"timeout" to nfcF.timeout.toString()
)
}
// NfcV (ISO 15693) 정보
val nfcV = NfcV.get(tag)
if (nfcV != null) {
tagInfo["NfcV"] = mapOf(
// 응답 플래그
"responseFlags" to String.format("%02X", nfcV.responseFlags),
// DSFID (Data Storage Format Identifier)
"dsfId" to String.format("%02X", nfcV.dsfId),
// 한 번에 전송할 수 있는 최대 데이터 길이 (바이트 단위)
"maxTransceiveLength" to nfcV.maxTransceiveLength
)
}
// NfcBarcode (Type V or JIS 6319-4) 정보 - maxTransceiveLength 지원 없음
val nfcBarcode = NfcBarcode.get(tag)
if (nfcBarcode != null) {
tagInfo["NfcBarcode"] = mapOf(
// 바코드 타입 정보 (Type V or JIS 6319-4)
"type" to nfcBarcode.type
)
}
// MifareClassic 정보
val mifareClassic = MifareClassic.get(tag)
if (mifareClassic != null) {
tagInfo["MifareClassic"] = mapOf(
// MifareClassic 타입 (Classic, Plus, Pro)
"type" to when (mifareClassic.type) {
MifareClassic.TYPE_CLASSIC -> "Classic"
MifareClassic.TYPE_PLUS -> "Plus"
MifareClassic.TYPE_PRO -> "Pro"
else -> "Unknown"
},
// MifareClassic 메모리 크기
"size" to mifareClassic.size,
// 섹터 개수
"sectorCount" to mifareClassic.sectorCount,
// 블록 개수
"blockCount" to mifareClassic.blockCount
)
}
// MifareUltralight 정보
val mifareUltralight = MifareUltralight.get(tag)
if (mifareUltralight != null) {
tagInfo["MifareUltralight"] = mapOf(
// MifareUltralight 타입 (Ultralight, Ultralight C)
"type" to when (mifareUltralight.type) {
MifareUltralight.TYPE_ULTRALIGHT -> "Ultralight"
MifareUltralight.TYPE_ULTRALIGHT_C -> "Ultralight C"
else -> "Unknown"
}
)
}
// JSON 형태로 변환하여 반환
return JSONObject(tagInfo as Map<*, *>).toString()
}
결과값 로그 출력 값은 아래와 같다.
로그 출력 값 예 :
{
"uid": "641B27A2B67881", // 시리얼 번호
"techList": "android.nfc.tech.IsoDep, android.nfc.tech.NfcA", // 태그에서 지원하는 NFC 기술 목록
"NfcA": { // NfcA (ISO/IEC 14443-3A) 기술에 대한 정보
"atqa": "4800", // Answer to Request, Type A (ATQA) 값
"sak": "20", // Select Acknowledge (SAK) 값
"maxTransceiveLength": 253, // NFC 통신에서 한 번에 전송할 수 있는 최대 데이터 길이(바이트)
"timeout": 618 // 통신 타임아웃 시간(밀리초)
},
"IsoDep": { // IsoDep (ISO/IEC 14443-4) 기술에 대한 정보
"historicalBytes": "", // NFC 태그가 최초로 통신을 시작할 때 제공하는 부가적인 정보를 포함
"hiLayerResponse": "N\/A", // 태그가 고급 프로토콜을 지원하는 경우 제공
"maxTransceiveLength": "65279",// IsoDep에서 한 번에 전송할 수 있는 최대 데이터 길이
"timeout": 618 // 통신 타임아웃 시간(밀리초)
}
}
디버그 서명 인증서는Android 앱을 개발하고 테스트할 때사용됩니다. Android 앱은 개발 중일 때와 실제 배포 시에 서명되어야 하는데, 이 두 경우에 각각 다른 인증서를 사용합니다. 디버그 서명 인증서는 앱이 아직 개발 중일 때, 주로디버깅과테스트를 위해 사용된다.
디버그 서명 인증서는Android 앱을 개발하고 테스트할 때사용된다. Android 앱은 개발 중일 때와 실제 배포 시에 서명되어야 하는데, 이 두 경우에 각각 다른 인증서를 사용한다. 디버그 서명 인증서는 앱이 아직 개발 중일 때, 주로디버깅과테스트를 위해 사용된다.
Firebase 설정에서 디버그 서명 인증서 사용은 Firebase Authentication 또는 Firebase Cloud Messaging(FCM)과 같은 기능을 테스트할 때, 디버그 서명 인증서의SHA-1해시 값이 필요하다. 이는 Firebase가 디버그 빌드에서도 Firebase 기능을 사용할 수 있도록 앱을 식별하기 위해 사용된다.
Android Studio를 사용하여 SHA-1 해시 값을 확인할 수 있는 방법이 두 가지 있다.
방법 1: Android Studio를 통해 SHA-1 값 확인
Android Studio에서Gradle창을 연다.
화면 우측의Gradle탭을 클릭. (보이지 않으면View>Tool Windows>Gradle)
Gradle Tasks 실행:
프로젝트 이름을 선택한 후,Tasks>android>signingReport를 더블 클릭.
SHA-1 값 확인:
signingReport를 실행하면디버그 및 릴리스 키의 SHA-1및SHA-256해시 값이 Android Studio의Run창에 출력된다.
아래와 같은 내용 출력: Variant: debug Config: debug Store:/Users/username/.android/debug.keystore Alias:AndroidDebugKey MD5:A1:B2:C3:... SHA1:AA:BB:CC:DD:... SHA-256:AB:CD:...
Android Gradle Plugin (AGP) 8.0 이상에서는 여러 가지 변화와 최적화가 이루어졌다. 이를 통해 성능 개선과 더불어 최신 Android SDK 및 Gradle 기능들을 더 잘 지원하게 되었다. Target SDK 34와 호환되며, 최적화되어 있다. Jetpack 및 라이브러리 호환성이 뛰어나며, 최신 Gradle 버전과 통합을 통해 Build 성능이 최적화 되었다. 아래는 AGP 8.0 이상에서 주목해야 할 주요 변경 사항들을 정리해 보았다.
1.compileSdkVersion및targetSdkVersion변경
이전:compileSdkVersion과targetSdkVersion을 사용하여 SDK 버전을 설정.
AGP 8.0 이상:compileSdk와targetSdk로 변경되었다.
android {
compileSdk = 34
targetSdk = 34
}
2.minSdkVersion변경
이전:minSdkVersion을 사용하여 최소 SDK 버전을 설정.
AGP 8.0 이상:minSdk로 변경되었다.
android {
minSdk = 21
}
3.buildToolsVersion자동 관리
이전:buildToolsVersion을 명시적으로 설정.
AGP 8.0 이상:Gradle이 자동으로 적절한buildTools버전을 선택하므로buildToolsVersion을 삭제해도 된다.
4.tasks.register방식 사용 권장
이전:태스크를 정의할 때task를 사용.
AGP 8.0 이상: 'tasks.register' 를 사용하는 것이 권장됨. 이는 Gradle의lazy configuration방식을 따르기 때문에 빌드 성능이 향상된다.
안드로이드에서 targetSdkVersion 34로 설정할 때는 몇 가지 주요 변경 사항과 주의 사항이 있습니다. 주요 사항은 다음과 같다.
1.권한 처리 강화
미디어 파일 권한: Android 14에서는 사진, 동영상, 오디오 파일에 대한 권한 요청이 분리되었습니다.READ_MEDIA_IMAGES,READ_MEDIA_VIDEO,READ_MEDIA_AUDIO와 같은 구체적인 권한을 사용해야 합니다.
알림 권한: Android 13(API 33)부터알림 사용을 위한 권한(POST_NOTIFICATIONS)이 필수적입니다. 사용자가 명시적으로 알림을 허용해야 앱이 알림을 보낼 수 있습니다.
예시:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100)
}
2.백그라운드 작업 제한 강화
정책 변경: Android 14는 백그라운드 작업에 대한 제약을 더 강화했습니다. 배터리 소모를 줄이기 위해 백그라운드에서 실행되는 작업에 대한 제한이 있으며, 특히앱이 백그라운드에서 실행 중일 때 작업 스케줄링이 더 어려워졌습니다. 이를 해결하기 위해JobScheduler또는 WorkManager를 사용하는 것이 권장됩니다.
3.기본 보안 정책 강화
SAF(Storage Access Framework) 적용 강화: 외부 저장소 접근이 더 제한적이며, 앱이 명시적으로 저장소 접근을 요청하지 않으면 사용자가 이를 거부할 수 있습니다.Scoped Storage적용을 준비해야 하며, 파일 관리는 앱 전용 저장소나 사용자 상호작용을 통해서만 접근해야 합니다.
4.암호화 및 보안
암호화된 네트워크 통신:명시적 암호화가 더 중요해졌습니다. Android 14는 더 엄격한 네트워크 보안 구성을 요구하며, 특히비암호화된 HTTP 요청을 차단하거나 이를 명시적으로 허용하지 않으면 기본적으로 차단됩니다. 앱의network_security_config.xml에서 설정을 확인하고 HTTPS를 권장합니다.
5.개발자 옵션 사용 제한
Android 14에서는 디버그 앱이 아닌 경우,앱 설치 후 개발자 옵션을 통해 설정된 일부 기능이 자동으로 차단될 수 있습니다. 따라서개발자 전용 빌드와배포 빌드에서 설정 차이를 명확히 하고, 배포 빌드에서는 사용자 경험에 영향을 주지 않도록 주의해야 합니다.
6.백그라운드 위치 접근 제한
위치 서비스 관련 변경 사항으로,백그라운드에서 위치 정보를 사용하는 경우에 대해 더 엄격한 권한 요구가 추가되었습니다. Android 10(API 29)부터 백그라운드 위치 권한(ACCESS_BACKGROUND_LOCATION)이 따로 분리되었으며, 이를 명시적으로 요청해야 합니다. 이제 위치 권한을 필요로 하는 경우,Foreground 권한 Background 권한을 각각 별도로 요청해야 합니다.
7.동적 코드 로딩 제한
Android 14에서는 동적 코드 로딩(dynamic code loading)과 관련된 보안 정책이 더 엄격해졌습니다. APK 내에서 동적으로 코드를 로드하는 방식(예: 외부에서 다운받은 Dex 파일 로딩)을 사용한다면, 보안상의 이유로 Google Play 정책 위반이 될 수 있으므로 주의해야 합니다.
8.Non-SDK 인터페이스 사용 제한
Android 14에서는Non-SDK 인터페이스(비공개 API)사용을 제한하는 정책이 강화되었습니다. 이제 비공개 API에 접근하는 것은 더욱 어렵고, 이에 대한 경고나 오류가 발생할 수 있으므로,공식 SDK를 사용하는지점검해야 합니다.