Initial commit: JRZ NFC Cloner

Android NFC app with read, write, clone and HCE emulation.
Supports NDEF, NFC-V (ISO 15693), MIFARE Ultralight and Classic.
Dark cyan Material3 theme with edge-to-edge layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 15:21:53 +02:00
commit f6bf1cb98f
48 changed files with 2916 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Gradle
.gradle/
build/
**/build/
# Android Studio
*.iml
.idea/
local.properties
*.DS_Store
# APK / Signing
*.apk
*.aab
*.keystore
*.jks
# Generated
app/src/main/assets/
+47
View File
@@ -0,0 +1,47 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.jrz.nfcapp"
compileSdk = 35
defaultConfig {
applicationId = "com.jrz.nfcapp"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.viewpager2)
}
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NFCApp">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- NFC Tag Discovery -->
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<!-- HCE Service -->
<service
android:name=".service.HceService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
</application>
</manifest>
@@ -0,0 +1,110 @@
package com.jrz.nfcapp.model
import android.nfc.NdefMessage
data class TagDump(
val uid: String,
val tagType: String,
val techList: List<String>,
val ndefMessage: NdefMessage?,
val ndefCapacity: Int,
val ndefMaxSize: Int,
val isNdefWritable: Boolean,
val rawPages: List<ByteArray>,
val rawBytes: ByteArray,
val debugLog: String = ""
) {
fun formattedUid(): String = uid.uppercase()
fun shortTechList(): String = techList
.map { it.substringAfterLast('.') }
.joinToString(", ")
fun ndefContentSummary(): String {
val msg = ndefMessage ?: return "Kein NDEF"
return buildString {
msg.records.forEachIndexed { i, record ->
append("Record $i: TNF=${record.tnf} ")
val type = String(record.type)
append("Type=$type ")
val payload = record.payload
when {
type == "T" -> {
// NDEF Text record
val langLen = payload[0].toInt() and 0x3F
append("\"${String(payload, langLen + 1, payload.size - langLen - 1)}\"\n")
}
type == "U" -> {
// NDEF URI record
val prefix = URI_PREFIXES.getOrElse(payload[0].toInt() and 0xFF) { "" }
append("$prefix${String(payload, 1, payload.size - 1)}\n")
}
else -> {
append("${payload.toHex()}\n")
}
}
}
}.trimEnd()
}
fun rawPagesFormatted(): String = buildString {
rawPages.forEachIndexed { i, page ->
append("Blk %03d: ".format(i))
append(page.toHex())
append(" ")
page.forEach { b ->
val c = (b.toInt() and 0xFF).toChar()
append(if (c.code in 32..126) c else '.')
}
append('\n')
}
}.trimEnd()
fun toCsvDump(): String = buildString {
appendLine("# NFC Tag Dump")
appendLine("UID,$uid")
appendLine("Type,$tagType")
appendLine("Technologies,${techList.joinToString("|")}")
appendLine("NDEF_Capacity,$ndefCapacity")
appendLine("NDEF_MaxSize,$ndefMaxSize")
appendLine("NDEF_Writable,$isNdefWritable")
appendLine()
appendLine("# Raw Pages")
rawPages.forEachIndexed { i, page ->
appendLine("Page_$i,${page.toHex()}")
}
if (ndefMessage != null) {
appendLine()
appendLine("# NDEF Records")
ndefMessage.records.forEachIndexed { i, rec ->
appendLine("Record_${i}_TNF,${rec.tnf}")
appendLine("Record_${i}_Type,${rec.type.toHex()}")
appendLine("Record_${i}_Payload,${rec.payload.toHex()}")
}
}
}
companion object {
val URI_PREFIXES = mapOf(
0x00 to "", 0x01 to "http://www.", 0x02 to "https://www.",
0x03 to "http://", 0x04 to "https://", 0x05 to "tel:",
0x06 to "mailto:", 0x07 to "ftp://anonymous:anonymous@",
0x08 to "ftp://ftp.", 0x09 to "ftps://", 0x0A to "sftp://",
0x0B to "smb://", 0x0C to "nfs://", 0x0D to "ftp://",
0x0E to "dav://", 0x0F to "news:", 0x10 to "telnet://",
0x11 to "imap:", 0x12 to "rtsp://", 0x13 to "urn:",
0x14 to "pop:", 0x15 to "sip:", 0x16 to "sips:",
0x17 to "tftp:", 0x18 to "btspp://", 0x19 to "btl2cap://",
0x1A to "btgoep://", 0x1B to "tcpobex://", 0x1C to "irdaobex://",
0x1D to "file://", 0x1E to "urn:epc:id:", 0x1F to "urn:epc:tag:",
0x20 to "urn:epc:pat:", 0x21 to "urn:epc:raw:", 0x22 to "urn:epc:",
0x23 to "urn:nfc:"
)
}
}
fun ByteArray.toHex(): String = joinToString(" ") { "%02X".format(it.toInt() and 0xFF) }
fun String.hexToBytes(): ByteArray {
val s = replace(" ", "").replace(":", "")
return ByteArray(s.length / 2) { i -> s.substring(i * 2, i * 2 + 2).toInt(16).toByte() }
}
@@ -0,0 +1,188 @@
package com.jrz.nfcapp.service
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log
import com.jrz.nfcapp.model.toHex
/**
* HCE-Dienst für NDEF-Tag-Emulation.
*
* Implementiert das NFC Forum Type 4 Tag Protokoll vereinfacht:
* 1. SELECT Application (AID)
* 2. SELECT Capability Container (CC)
* 3. READ BINARY CC
* 4. SELECT NDEF File
* 5. READ BINARY NDEF
*
* Nur geeignet für Systeme, die AID/APDU-basiert kommunizieren.
* UID-basierte Systeme (Zugangskontrolle) werden NICHT unterstützt.
*/
class HceService : HostApduService() {
companion object {
private const val TAG = "HceService"
// Status words
private val SW_OK = byteArrayOf(0x90.toByte(), 0x00)
private val SW_NOT_FOUND = byteArrayOf(0x6A.toByte(), 0x82.toByte())
private val SW_WRONG_LENGTH = byteArrayOf(0x67.toByte(), 0x00)
private val SW_UNKNOWN = byteArrayOf(0x6F.toByte(), 0x00)
// File IDs
private val CC_FILE_ID = byteArrayOf(0xE1.toByte(), 0x03)
private val NDEF_FILE_ID = byteArrayOf(0xE1.toByte(), 0x04)
// Selected file state
private const val SEL_NONE = 0
private const val SEL_CC = 1
private const val SEL_NDEF = 2
// Singleton payload + log callback (set from HceFragment)
@Volatile var ndefPayload: ByteArray = buildDefaultNdefPayload()
var apduLogCallback: ((String) -> Unit)? = null
fun buildDefaultNdefPayload(): ByteArray {
// NDEF Text record: "NFC Tool"
val text = "NFC Tool"
val lang = "de"
val langBytes = lang.toByteArray()
val textBytes = text.toByteArray()
val recordPayload = ByteArray(1 + langBytes.size + textBytes.size)
recordPayload[0] = langBytes.size.toByte()
langBytes.copyInto(recordPayload, 1)
textBytes.copyInto(recordPayload, 1 + langBytes.size)
// NDEF record header: MB=1 ME=1 SR=1 TNF=1 (Well-Known)
val typeBytes = byteArrayOf(0x54) // 'T'
val recordHeader = byteArrayOf(
0xD1.toByte(), // MB=1 ME=1 SR=1 TNF=0x01
typeBytes.size.toByte(), // type length
recordPayload.size.toByte() // short record payload length
)
val ndefRecord = recordHeader + typeBytes + recordPayload
// NDEF file: 2-byte length + record
return byteArrayOf(
0x00, ndefRecord.size.toByte()
) + ndefRecord
}
}
private var selectedFile = SEL_NONE
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
val hex = commandApdu.toHex()
log("CMD: $hex")
if (commandApdu.size < 4) {
log("RSP: Wrong length")
return SW_WRONG_LENGTH
}
val cla = commandApdu[0].toInt() and 0xFF
val ins = commandApdu[1].toInt() and 0xFF
val p1 = commandApdu[2].toInt() and 0xFF
val p2 = commandApdu[3].toInt() and 0xFF
return when {
ins == 0xA4 -> handleSelect(commandApdu, p1, p2)
ins == 0xB0 -> handleReadBinary(commandApdu, p1, p2)
else -> {
log("RSP: Unknown INS ${"%02X".format(ins)}")
SW_UNKNOWN
}
}
}
private fun handleSelect(apdu: ByteArray, p1: Int, p2: Int): ByteArray {
return when {
// SELECT Application by AID (P1=0x04)
p1 == 0x04 -> {
log("RSP: SELECT Application -> OK")
selectedFile = SEL_NONE
SW_OK
}
// SELECT File by ID (P1=0x00 or 0x02)
p1 == 0x00 || p1 == 0x02 -> {
if (apdu.size < 7) return SW_WRONG_LENGTH
val fileId = byteArrayOf(apdu[5], apdu[6])
when {
fileId.contentEquals(CC_FILE_ID) -> {
selectedFile = SEL_CC
log("RSP: SELECT CC -> OK")
SW_OK
}
fileId.contentEquals(NDEF_FILE_ID) -> {
selectedFile = SEL_NDEF
log("RSP: SELECT NDEF -> OK")
SW_OK
}
else -> {
log("RSP: SELECT unknown file ${fileId.toHex()} -> NOT FOUND")
SW_NOT_FOUND
}
}
}
else -> {
log("RSP: SELECT unknown P1=${"%02X".format(p1)} -> NOT FOUND")
SW_NOT_FOUND
}
}
}
private fun handleReadBinary(apdu: ByteArray, p1: Int, p2: Int): ByteArray {
val offset = (p1 shl 8) or p2
val length = if (apdu.size >= 5) apdu[4].toInt() and 0xFF else 15
return when (selectedFile) {
SEL_CC -> {
val cc = buildCapabilityContainer()
val end = minOf(offset + length, cc.size)
if (offset >= cc.size) return SW_WRONG_LENGTH
val data = cc.copyOfRange(offset, end)
log("RSP: READ CC offset=$offset len=$length -> ${data.toHex()} 90 00")
data + SW_OK
}
SEL_NDEF -> {
val ndef = ndefPayload
val end = minOf(offset + length, ndef.size)
if (offset >= ndef.size) return SW_WRONG_LENGTH
val data = ndef.copyOfRange(offset, end)
log("RSP: READ NDEF offset=$offset len=$length -> ${data.toHex()} 90 00")
data + SW_OK
}
else -> {
log("RSP: READ without SELECT -> NOT FOUND")
SW_NOT_FOUND
}
}
}
private fun buildCapabilityContainer(): ByteArray {
val ndefSize = ndefPayload.size
return byteArrayOf(
0x00, 0x0F, // CC length = 15 bytes
0x20, // Mapping version 2.0
0x00, 0x3B, // MLe (max R-APDU): 59 bytes
0x00, 0x34, // MLc (max C-APDU): 52 bytes
0x04, // NDEF File Control TLV tag
0x06, // TLV length
NDEF_FILE_ID[0], NDEF_FILE_ID[1], // File ID
0x00, ndefSize.toByte(),// Max NDEF size
0x00, // Read access: open
0xFF.toByte() // Write access: no write (read-only HCE)
)
}
private fun log(msg: String) {
Log.d(TAG, msg)
apduLogCallback?.invoke(msg)
}
override fun onDeactivated(reason: Int) {
val why = if (reason == DEACTIVATION_LINK_LOSS) "Link-Verlust" else "Neues Tag"
log("--- Deaktiviert: $why ---")
selectedFile = SEL_NONE
}
}
@@ -0,0 +1,130 @@
package com.jrz.nfcapp.ui
import android.app.PendingIntent
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import com.jrz.nfcapp.R
import com.jrz.nfcapp.databinding.ActivityMainBinding
import com.jrz.nfcapp.ui.clone.CloneFragment
import com.jrz.nfcapp.ui.hce.HceFragment
import com.jrz.nfcapp.ui.read.ReadFragment
import com.jrz.nfcapp.ui.write.WriteFragment
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var nfcAdapter: NfcAdapter? = null
private var pendingIntent: PendingIntent? = null
private lateinit var readFragment: ReadFragment
private lateinit var writeFragment: WriteFragment
private lateinit var cloneFragment: CloneFragment
private lateinit var hceFragment: HceFragment
override fun onCreate(savedInstanceState: Bundle?) {
// Edge-to-edge: content draws behind status & nav bar
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Apply insets: AppBarLayout handles top via fitsSystemWindows,
// ViewPager gets bottom padding for the navigation bar
ViewCompat.setOnApplyWindowInsetsListener(binding.viewPager) { view, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(bottom = bars.bottom)
insets
}
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter == null) {
Toast.makeText(this, getString(R.string.nfc_not_supported), Toast.LENGTH_LONG).show()
updateNfcBadge(supported = false)
} else {
updateNfcBadge(supported = true)
}
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
setupViewPager()
if (intent.hasExtra(NfcAdapter.EXTRA_TAG)) {
onNewIntent(intent)
}
}
private fun updateNfcBadge(supported: Boolean) {
if (!supported) {
binding.tvNfcStatus.text = "KEIN NFC"
binding.nfcStatusDot.setBackgroundResource(R.drawable.dot_inactive)
}
}
private fun setupViewPager() {
readFragment = ReadFragment()
writeFragment = WriteFragment()
cloneFragment = CloneFragment()
hceFragment = HceFragment()
val fragments = listOf(readFragment, writeFragment, cloneFragment, hceFragment)
val titles = listOf(
getString(R.string.tab_read),
getString(R.string.tab_write),
getString(R.string.tab_clone),
getString(R.string.tab_hce)
)
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun getItemCount() = fragments.size
override fun createFragment(position: Int) = fragments[position]
}
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = titles[position]
}.attach()
}
override fun onResume() {
super.onResume()
val adapter = nfcAdapter ?: return
if (!adapter.isEnabled) {
Toast.makeText(this, getString(R.string.nfc_disabled), Toast.LENGTH_LONG).show()
binding.tvNfcStatus.text = "AUS"
binding.nfcStatusDot.setBackgroundResource(R.drawable.dot_inactive)
return
}
binding.tvNfcStatus.text = "NFC"
binding.nfcStatusDot.setBackgroundResource(R.drawable.dot_active)
adapter.enableForegroundDispatch(this, pendingIntent, null, null)
}
override fun onPause() {
super.onPause()
nfcAdapter?.disableForegroundDispatch(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
when (binding.viewPager.currentItem) {
0 -> readFragment.onTagScanned(tag)
1 -> writeFragment.onTagScanned(tag)
2 -> cloneFragment.onTagScanned(tag)
}
}
}
@@ -0,0 +1,140 @@
package com.jrz.nfcapp.ui.clone
import android.nfc.Tag
import java.io.IOException
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.jrz.nfcapp.R
import com.jrz.nfcapp.databinding.FragmentCloneBinding
import com.jrz.nfcapp.model.TagDump
import com.jrz.nfcapp.util.NfcHelper
enum class CloneState { IDLE, READING_SOURCE, WRITING_TARGET }
class CloneFragment : Fragment() {
private var _binding: FragmentCloneBinding? = null
private val binding get() = _binding!!
var state: CloneState = CloneState.IDLE
private set
private var sourceDump: TagDump? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCloneBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnReadSource.setOnClickListener {
state = CloneState.READING_SOURCE
binding.tvSourceStatus.text = getString(R.string.clone_waiting_source)
binding.btnReadSource.isEnabled = false
}
binding.btnWriteTarget.setOnClickListener {
if (sourceDump == null) return@setOnClickListener
state = CloneState.WRITING_TARGET
binding.tvTargetStatus.text = getString(R.string.clone_waiting_target)
binding.btnWriteTarget.isEnabled = false
}
}
fun onTagScanned(tag: Tag) {
when (state) {
CloneState.READING_SOURCE -> readSource(tag)
CloneState.WRITING_TARGET -> writeTarget(tag)
CloneState.IDLE -> {}
}
}
private fun readSource(tag: Tag) {
Thread {
try {
val dump = NfcHelper.readTag(tag)
sourceDump = dump
requireActivity().runOnUiThread { onSourceRead(dump) }
} catch (e: Exception) {
requireActivity().runOnUiThread {
state = CloneState.IDLE
binding.tvSourceStatus.text = getString(R.string.error_read, e.message)
binding.btnReadSource.isEnabled = true
}
}
}.start()
}
private fun onSourceRead(dump: TagDump) {
state = CloneState.IDLE
binding.btnReadSource.isEnabled = true
binding.tvSourceStatus.text = getString(R.string.clone_source_ready, dump.rawBytes.size)
binding.layoutSourceDetails.visibility = View.VISIBLE
binding.tvSourceUid.text = "UID: ${dump.formattedUid()}"
binding.tvSourceType.text = "Typ: ${dump.tagType}"
val sizeInfo = buildString {
if (dump.ndefMessage != null) append("NDEF: ${dump.ndefMessage.byteArrayLength} Bytes ")
if (dump.rawPages.isNotEmpty()) append("Pages: ${dump.rawPages.size} (${dump.rawBytes.size} Bytes)")
if (dump.ndefMessage == null && dump.rawPages.isEmpty()) append("Keine lesbaren Daten")
}
binding.tvSourceSize.text = sizeInfo
binding.btnWriteTarget.isEnabled = dump.ndefMessage != null || dump.rawPages.isNotEmpty()
binding.tvTargetStatus.text = if (binding.btnWriteTarget.isEnabled)
"Bereit zum Schreiben" else "Keine schreibbaren Daten"
}
private fun writeTarget(tag: Tag) {
val dump = sourceDump ?: return
Thread {
try {
when (NfcHelper.detectWriteMethod(tag)) {
NfcHelper.WriteMethod.NFC_V -> {
if (dump.rawPages.isEmpty()) throw IOException("Keine Quelldaten zum Schreiben")
NfcHelper.writeNfcV(tag, dump.rawPages)
}
NfcHelper.WriteMethod.ULTRALIGHT -> {
if (dump.rawPages.isEmpty()) throw IOException("Keine Quelldaten zum Schreiben")
NfcHelper.writeUltralight(tag, dump.rawPages)
}
NfcHelper.WriteMethod.NDEF,
NfcHelper.WriteMethod.NDEF_FORMATABLE -> {
val msg = dump.ndefMessage ?: throw IOException("Keine NDEF-Daten in Quelle")
NfcHelper.writeNdef(tag, msg)
}
NfcHelper.WriteMethod.MIFARE_CLASSIC ->
throw IOException("MIFARE Classic Schreiben benötigt Schlüssel")
NfcHelper.WriteMethod.UNSUPPORTED ->
throw IOException("Ziel-Tag unterstützt kein Schreiben")
}
requireActivity().runOnUiThread { onWriteSuccess() }
} catch (e: Exception) {
requireActivity().runOnUiThread { onWriteError(e.message ?: "Fehler") }
}
}.start()
}
private fun onWriteSuccess() {
state = CloneState.IDLE
binding.tvTargetStatus.text = getString(R.string.clone_success)
binding.btnWriteTarget.isEnabled = true
}
private fun onWriteError(error: String) {
state = CloneState.IDLE
binding.tvTargetStatus.text = getString(R.string.error_write, error)
binding.btnWriteTarget.isEnabled = true
}
// Extension for display
private fun TagDump.formattedUid() = uid.uppercase()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,120 @@
package com.jrz.nfcapp.ui.hce
import android.content.ComponentName
import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.cardemulation.CardEmulation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.jrz.nfcapp.R
import com.jrz.nfcapp.databinding.FragmentHceBinding
import com.jrz.nfcapp.service.HceService
class HceFragment : Fragment() {
private var _binding: FragmentHceBinding? = null
private val binding get() = _binding!!
private var isActive = false
private val apduLogLines = ArrayDeque<String>(200)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentHceBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnEnableHce.setOnClickListener { enableHce() }
binding.btnDisableHce.setOnClickListener { disableHce() }
binding.btnClearLog.setOnClickListener {
apduLogLines.clear()
binding.tvApduLog.text = "Kein APDU-Verkehr…"
}
// Register log callback
HceService.apduLogCallback = { line ->
requireActivity().runOnUiThread { appendLog(line) }
}
}
private fun enableHce() {
val aid = binding.etAid.text?.toString()?.trim()?.replace(" ", "")?.uppercase() ?: ""
val payloadText = binding.etPayload.text?.toString()?.trim() ?: ""
if (aid.isEmpty()) {
Snackbar.make(binding.root, "Bitte AID eingeben", Snackbar.LENGTH_SHORT).show()
return
}
if (aid.length % 2 != 0 || aid.length < 10 || aid.length > 32) {
Snackbar.make(binding.root, "AID muss 5-16 Hex-Bytes sein", Snackbar.LENGTH_LONG).show()
return
}
// Build NDEF payload
HceService.ndefPayload = buildNdefPayload(payloadText)
isActive = true
updateUi()
appendLog("=== HCE aktiviert, AID: $aid ===")
Snackbar.make(binding.root, "HCE aktiv. Halte das Telefon an ein Lesegerät.", Snackbar.LENGTH_LONG).show()
}
private fun disableHce() {
isActive = false
updateUi()
appendLog("=== HCE deaktiviert ===")
}
private fun updateUi() {
binding.hceStatusDot.setBackgroundResource(
if (isActive) R.drawable.dot_active else R.drawable.dot_inactive
)
binding.tvHceStatus.text = getString(
if (isActive) R.string.hce_status_active else R.string.hce_status_inactive
)
binding.btnEnableHce.isEnabled = !isActive
binding.btnDisableHce.isEnabled = isActive
binding.etAid.isEnabled = !isActive
binding.etPayload.isEnabled = !isActive
}
private fun appendLog(line: String) {
apduLogLines.addLast(line)
if (apduLogLines.size > 100) apduLogLines.removeFirst()
binding.tvApduLog.text = apduLogLines.joinToString("\n")
}
private fun buildNdefPayload(text: String): ByteArray {
if (text.isEmpty()) return HceService.buildDefaultNdefPayload()
val lang = "de"
val langBytes = lang.toByteArray()
val textBytes = text.toByteArray(Charsets.UTF_8)
val recordPayload = ByteArray(1 + langBytes.size + textBytes.size)
recordPayload[0] = langBytes.size.toByte()
langBytes.copyInto(recordPayload, 1)
textBytes.copyInto(recordPayload, 1 + langBytes.size)
val typeBytes = byteArrayOf(0x54)
val ndefRecord = byteArrayOf(
0xD1.toByte(),
typeBytes.size.toByte(),
recordPayload.size.toByte()
) + typeBytes + recordPayload
return byteArrayOf(0x00, ndefRecord.size.toByte()) + ndefRecord
}
override fun onDestroyView() {
HceService.apduLogCallback = null
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,174 @@
package com.jrz.nfcapp.ui.read
import android.animation.ValueAnimator
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.nfc.Tag
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.jrz.nfcapp.R
import com.jrz.nfcapp.databinding.FragmentReadBinding
import com.jrz.nfcapp.model.TagDump
import com.jrz.nfcapp.util.NfcHelper
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class ReadFragment : Fragment() {
private var _binding: FragmentReadBinding? = null
private val binding get() = _binding!!
private var currentDump: TagDump? = null
private var pulseAnimator: ValueAnimator? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentReadBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnCopyUid.setOnClickListener {
currentDump?.let { dump ->
val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(ClipData.newPlainText("NFC UID", dump.formattedUid()))
Snackbar.make(binding.root, getString(R.string.uid_copied), Snackbar.LENGTH_SHORT).show()
}
}
binding.btnSaveDump.setOnClickListener {
currentDump?.let { saveDump(it) }
}
startPulseAnimation()
}
override fun onResume() {
super.onResume()
if (currentDump == null) startPulseAnimation()
}
override fun onPause() {
super.onPause()
stopPulseAnimation()
}
private fun startPulseAnimation() {
pulseAnimator?.cancel()
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 2000
repeatCount = ValueAnimator.INFINITE
interpolator = DecelerateInterpolator()
addUpdateListener { anim ->
val p = anim.animatedValue as Float
// Ring 1: leads
val p1 = p
binding.ring1.alpha = (1f - p1) * 0.7f
binding.ring1.scaleX = 0.3f + p1 * 0.7f
binding.ring1.scaleY = 0.3f + p1 * 0.7f
// Ring 2: delayed by 33%
val p2 = ((p + 0.33f) % 1f)
binding.ring2.alpha = (1f - p2) * 0.5f
binding.ring2.scaleX = 0.3f + p2 * 0.7f
binding.ring2.scaleY = 0.3f + p2 * 0.7f
// Ring 3: delayed by 66%
val p3 = ((p + 0.66f) % 1f)
binding.ring3.alpha = (1f - p3) * 0.3f
binding.ring3.scaleX = 0.3f + p3 * 0.7f
binding.ring3.scaleY = 0.3f + p3 * 0.7f
}
}
animator.start()
pulseAnimator = animator
}
private fun stopPulseAnimation() {
pulseAnimator?.cancel()
pulseAnimator = null
listOf(binding.ring1, binding.ring2, binding.ring3).forEach { it.alpha = 0f }
}
fun onTagScanned(tag: Tag) {
try {
val dump = NfcHelper.readTag(tag)
currentDump = dump
requireActivity().runOnUiThread { displayDump(dump) }
} catch (e: Exception) {
requireActivity().runOnUiThread {
binding.tvStatus.text = getString(R.string.error_read, e.message)
}
}
}
private fun displayDump(dump: TagDump) {
stopPulseAnimation()
binding.tvStatus.text = "Tag erkannt · ${dump.rawBytes.size} Bytes"
// UID
binding.cardUid.visibility = View.VISIBLE
binding.tvUid.text = dump.formattedUid()
// Tag Type
binding.cardTagType.visibility = View.VISIBLE
binding.tvTagType.text = dump.tagType
binding.tvTechList.text = dump.techList.joinToString("\n") { "${it.substringAfterLast('.')}" }
// NDEF
if (dump.ndefMessage != null || dump.ndefCapacity > 0) {
binding.cardNdef.visibility = View.VISIBLE
binding.tvNdefContent.text = buildString {
appendLine(dump.ndefContentSummary())
if (dump.ndefMaxSize > 0) {
appendLine("\nKapazität: ${dump.ndefCapacity} / ${dump.ndefMaxSize} Bytes")
append("Schreibbar: ${if (dump.isNdefWritable) "Ja" else "Nein"}")
}
}.trimEnd()
}
// Raw + Diagnose log
binding.cardRaw.visibility = View.VISIBLE
binding.tvRawPages.text = buildString {
if (dump.rawPages.isNotEmpty()) {
appendLine(dump.rawPagesFormatted())
appendLine()
}
appendLine("── Diagnose ──")
append(dump.debugLog)
}
binding.btnSaveDump.visibility = View.VISIBLE
}
private fun saveDump(dump: TagDump) {
try {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val filename = "jrz_dump_${sdf.format(Date())}.csv"
val dir = requireContext().getExternalFilesDir(null) ?: requireContext().filesDir
val file = File(dir, filename)
file.writeText(dump.toCsvDump())
Snackbar.make(binding.root, getString(R.string.dump_saved, file.absolutePath), Snackbar.LENGTH_LONG).show()
} catch (e: Exception) {
Snackbar.make(binding.root, "Speichern fehlgeschlagen: ${e.message}", Snackbar.LENGTH_LONG).show()
}
}
override fun onDestroyView() {
stopPulseAnimation()
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,137 @@
package com.jrz.nfcapp.ui.write
import android.nfc.NdefMessage
import android.nfc.Tag
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.jrz.nfcapp.R
import com.jrz.nfcapp.databinding.FragmentWriteBinding
import com.jrz.nfcapp.util.NfcHelper
class WriteFragment : Fragment() {
private var _binding: FragmentWriteBinding? = null
private val binding get() = _binding!!
var isWaitingForTag = false
private set
private var pendingMessage: NdefMessage? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentWriteBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.etContent.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
updateByteCount()
}
})
binding.btnWrite.setOnClickListener { startWrite() }
binding.btnCancelWrite.setOnClickListener { cancelWrite() }
}
private fun updateByteCount() {
val content = binding.etContent.text?.toString() ?: ""
val bytes = try {
buildMessage(content).byteArrayLength
} catch (_: Exception) { content.length }
binding.tvByteCount.text = "$bytes Bytes"
}
private fun startWrite() {
val content = binding.etContent.text?.toString()?.trim() ?: ""
if (content.isEmpty()) {
Snackbar.make(binding.root, "Bitte Inhalt eingeben", Snackbar.LENGTH_SHORT).show()
return
}
pendingMessage = try {
buildMessage(content)
} catch (e: Exception) {
Snackbar.make(binding.root, "Ungültiger Inhalt: ${e.message}", Snackbar.LENGTH_LONG).show()
return
}
isWaitingForTag = true
binding.btnWrite.isEnabled = false
binding.btnCancelWrite.visibility = View.VISIBLE
binding.writeStatusCard.visibility = View.VISIBLE
binding.tvWriteStatus.text = getString(R.string.write_waiting)
}
private fun cancelWrite() {
isWaitingForTag = false
pendingMessage = null
binding.btnWrite.isEnabled = true
binding.btnCancelWrite.visibility = View.GONE
binding.writeStatusCard.visibility = View.GONE
}
fun onTagScanned(tag: Tag) {
if (!isWaitingForTag) return
val msg = pendingMessage ?: return
Thread {
try {
when (NfcHelper.detectWriteMethod(tag)) {
NfcHelper.WriteMethod.NFC_V -> {
// NFC-V: erst als NDEF formatieren (NdefFormatable), dann NDEF schreiben
// Falls NdefFormatable vorhanden → format() macht beides in einem Schritt
NfcHelper.writeNdef(tag, msg)
}
NfcHelper.WriteMethod.MIFARE_CLASSIC ->
throw java.io.IOException("MIFARE Classic benötigt Schlüssel zum Schreiben")
NfcHelper.WriteMethod.UNSUPPORTED ->
throw java.io.IOException("Dieser Tag unterstützt kein Schreiben")
else -> NfcHelper.writeNdef(tag, msg)
}
requireActivity().runOnUiThread { onWriteSuccess() }
} catch (e: Exception) {
requireActivity().runOnUiThread { onWriteError(e.message ?: "Unbekannter Fehler") }
}
}.start()
}
private fun onWriteSuccess() {
isWaitingForTag = false
pendingMessage = null
binding.btnWrite.isEnabled = true
binding.btnCancelWrite.visibility = View.GONE
binding.tvWriteStatus.text = getString(R.string.write_success)
binding.writeStatusCard.setCardBackgroundColor(resources.getColor(R.color.success_green, null))
}
private fun onWriteError(error: String) {
isWaitingForTag = false
pendingMessage = null
binding.btnWrite.isEnabled = true
binding.btnCancelWrite.visibility = View.GONE
binding.tvWriteStatus.text = getString(R.string.error_write, error)
binding.writeStatusCard.setCardBackgroundColor(resources.getColor(R.color.error_red, null))
}
private fun buildMessage(content: String): NdefMessage = when {
binding.chipUrl.isChecked -> NfcHelper.buildUriNdef(content)
binding.chipHex.isChecked -> NfcHelper.buildRawNdef(content)
else -> NfcHelper.buildTextNdef(content)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,342 @@
package com.jrz.nfcapp.util
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.Tag
import android.nfc.tech.*
import com.jrz.nfcapp.model.TagDump
import com.jrz.nfcapp.model.toHex
import java.io.IOException
object NfcHelper {
// Standard MIFARE Classic default keys
private val MIFARE_DEFAULT_KEYS = listOf(
byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()),
byteArrayOf(0xA0.toByte(), 0xA1.toByte(), 0xA2.toByte(), 0xA3.toByte(), 0xA4.toByte(), 0xA5.toByte()),
byteArrayOf(0xD3.toByte(), 0xF7.toByte(), 0xD3.toByte(), 0xF7.toByte(), 0xD3.toByte(), 0xF7.toByte()),
byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
byteArrayOf(0xB0.toByte(), 0xB1.toByte(), 0xB2.toByte(), 0xB3.toByte(), 0xB4.toByte(), 0xB5.toByte()),
byteArrayOf(0x4D.toByte(), 0x3A.toByte(), 0x99.toByte(), 0xC3.toByte(), 0x51.toByte(), 0xDD.toByte()),
byteArrayOf(0x1A.toByte(), 0x98.toByte(), 0x2C.toByte(), 0x7E.toByte(), 0x45.toByte(), 0x9A.toByte()),
byteArrayOf(0xAA.toByte(), 0xBB.toByte(), 0xCC.toByte(), 0xDD.toByte(), 0xEE.toByte(), 0xFF.toByte())
)
fun readTag(tag: Tag): TagDump {
val uid = tag.id.toHex()
val techList = tag.techList.toList()
var ndefMsg: NdefMessage? = null
var ndefCapacity = 0
var ndefMaxSize = 0
var ndefWritable = false
val rawPages = mutableListOf<ByteArray>()
var tagType = detectTagType(techList)
val log = StringBuilder()
log.appendLine("UID: $uid")
log.appendLine("Technologien: ${techList.map { it.substringAfterLast('.') }}")
// --- NDEF lesen ---
if (techList.contains(Ndef::class.java.name)) {
log.appendLine("→ Versuche NDEF lesen…")
val ndef = Ndef.get(tag)
try {
ndef.connect()
ndefMsg = ndef.cachedNdefMessage
if (ndefMsg == null) {
log.appendLine(" cachedNdefMessage null, lese live…")
ndefMsg = ndef.ndefMessage
}
ndefMaxSize = ndef.maxSize
ndefWritable = ndef.isWritable
ndefCapacity = ndef.maxSize
tagType = ndef.type ?: tagType
if (ndefMsg != null) {
log.appendLine(" NDEF OK: ${ndefMsg.records.size} Record(s), ${ndefMsg.byteArrayLength} Bytes")
} else {
log.appendLine(" NDEF leer (kein Inhalt)")
}
} catch (e: Exception) {
log.appendLine(" NDEF Fehler: ${e.javaClass.simpleName}: ${e.message}")
} finally {
try { ndef.close() } catch (_: Exception) {}
}
}
// --- MIFARE Ultralight lesen ---
if (techList.contains(MifareUltralight::class.java.name)) {
log.appendLine("→ Versuche MifareUltralight lesen…")
val ul = MifareUltralight.get(tag)
try {
ul.connect()
val pageCount = when (ul.type) {
MifareUltralight.TYPE_ULTRALIGHT -> 16
MifareUltralight.TYPE_ULTRALIGHT_C -> 48
else -> 45 // NTAG213=45, NTAG215=135, NTAG216=231 — start conservative
}
log.appendLine(" Typ: ${ul.type}, lese $pageCount Pages…")
var readCount = 0
for (page in 0 until pageCount) {
try {
rawPages.add(ul.readPages(page).copyOfRange(0, 4))
readCount++
} catch (e: IOException) {
log.appendLine(" Stop bei Page $page: ${e.message}")
break
}
}
log.appendLine(" $readCount Pages gelesen")
} catch (e: Exception) {
log.appendLine(" Ultralight Fehler: ${e.javaClass.simpleName}: ${e.message}")
} finally {
try { ul.close() } catch (_: Exception) {}
}
}
// --- MIFARE Classic lesen (Default-Keys) ---
if (techList.contains(MifareClassic::class.java.name)) {
log.appendLine("→ Versuche MifareClassic mit Default-Keys…")
val mc = MifareClassic.get(tag)
try {
mc.connect()
val sectorCount = mc.sectorCount
log.appendLine(" ${mc.type}$sectorCount Sektoren, ${mc.blockCount} Blöcke")
var unlockedSectors = 0
for (sector in 0 until sectorCount) {
var authenticated = false
for (key in MIFARE_DEFAULT_KEYS) {
try {
if (mc.authenticateSectorWithKeyA(sector, key) ||
mc.authenticateSectorWithKeyB(sector, key)) {
authenticated = true
break
}
} catch (_: Exception) {}
}
if (authenticated) {
unlockedSectors++
val blockCount = mc.getBlockCountInSector(sector)
val firstBlock = mc.sectorToBlock(sector)
for (b in 0 until blockCount) {
try {
val blockData = mc.readBlock(firstBlock + b)
rawPages.add(blockData)
} catch (e: IOException) {
rawPages.add(ByteArray(16) { 0xFF.toByte() })
}
}
} else {
log.appendLine(" Sektor $sector: kein Default-Key passt (verschlüsselt)")
// Füge Platzhalt-Blöcke ein damit Sektornummern stimmen
val blockCount = mc.getBlockCountInSector(sector)
repeat(blockCount) { rawPages.add(ByteArray(16) { 0xEE.toByte() }) }
}
}
log.appendLine(" $unlockedSectors/$sectorCount Sektoren entsperrt")
} catch (e: Exception) {
log.appendLine(" MifareClassic Fehler: ${e.javaClass.simpleName}: ${e.message}")
} finally {
try { mc.close() } catch (_: Exception) {}
}
}
// --- NfcA Diagnostik (ATQA/SAK) ---
if (techList.contains(NfcA::class.java.name)) {
val nfcA = NfcA.get(tag)
try {
nfcA.connect()
val atqa = nfcA.atqa.toHex()
val sak = "%02X".format(nfcA.sak)
log.appendLine("→ NfcA: ATQA=$atqa SAK=$sak")
} catch (e: Exception) {
log.appendLine("→ NfcA Diagnostik Fehler: ${e.message}")
} finally {
try { nfcA.close() } catch (_: Exception) {}
}
}
// --- NFC-V (ISO 15693) lesen ---
if (techList.contains(NfcV::class.java.name) && rawPages.isEmpty()) {
log.appendLine("→ Versuche NFC-V (ISO 15693) lesen…")
val nfcV = NfcV.get(tag)
try {
nfcV.connect()
val dsfId = nfcV.dsfId
val responseFlags = nfcV.responseFlags
log.appendLine(" DSFID=$dsfId ResponseFlags=$responseFlags")
// READ_SINGLE_BLOCK: flags=0x02, cmd=0x20, blockNum
// Flags 0x02 = high data rate, non-addressed mode
var block = 0
var consecutiveErrors = 0
while (block < 256 && consecutiveErrors < 3) {
try {
val cmd = byteArrayOf(0x02, 0x20, block.toByte())
val response = nfcV.transceive(cmd)
if (response.isNotEmpty() && (response[0].toInt() and 0x01) == 0) {
// Erfolg: response[0] = flags, rest = block data
val blockData = response.copyOfRange(1, response.size)
rawPages.add(blockData)
consecutiveErrors = 0
} else {
consecutiveErrors++
}
} catch (e: IOException) {
consecutiveErrors++
if (block == 0) {
log.appendLine(" Block 0 Fehler: ${e.message} — versuche addressed mode…")
// Versuche addressed mode (flags=0x22 + UID)
try {
val uidBytes = tag.id
val cmd = byteArrayOf(0x22, 0x20) + uidBytes + byteArrayOf(0x00)
val response = nfcV.transceive(cmd)
if (response.isNotEmpty() && (response[0].toInt() and 0x01) == 0) {
rawPages.add(response.copyOfRange(1, response.size))
consecutiveErrors = 0
log.appendLine(" Addressed mode erfolgreich")
}
} catch (_: Exception) {}
}
}
block++
}
log.appendLine(" ${rawPages.size} Blöcke gelesen")
} catch (e: Exception) {
log.appendLine(" NfcV Fehler: ${e.javaClass.simpleName}: ${e.message}")
} finally {
try { nfcV.close() } catch (_: Exception) {}
}
}
// --- IsoDep / ISO 14443-4 ---
if (techList.contains(IsoDep::class.java.name) && ndefMsg == null && rawPages.isEmpty()) {
log.appendLine("→ IsoDep erkannt — kein automatisches Lesen möglich (proprietäres Protokoll)")
}
val rawBytes = when {
rawPages.isNotEmpty() -> rawPages.flatMap { it.toList() }.toByteArray()
ndefMsg != null -> ndefMsg.toByteArray()
else -> ByteArray(0)
}
log.appendLine("─── Gesamt: ${rawBytes.size} Bytes ───")
return TagDump(
uid = uid,
tagType = tagType,
techList = techList,
ndefMessage = ndefMsg,
ndefCapacity = ndefCapacity,
ndefMaxSize = ndefMaxSize,
isNdefWritable = ndefWritable,
rawPages = rawPages,
rawBytes = rawBytes,
debugLog = log.toString().trimEnd()
)
}
fun writeNdef(tag: Tag, message: NdefMessage) {
if (tag.techList.contains(Ndef::class.java.name)) {
val ndef = Ndef.get(tag)
ndef.connect()
try {
if (!ndef.isWritable) throw IOException("Tag ist schreibgeschützt")
if (ndef.maxSize < message.byteArrayLength) {
throw IOException("Tag zu klein: ${ndef.maxSize} Bytes verfügbar, ${message.byteArrayLength} benötigt")
}
ndef.writeNdefMessage(message)
} finally {
ndef.close()
}
} else if (tag.techList.contains(NdefFormatable::class.java.name)) {
val formatable = NdefFormatable.get(tag)
formatable.connect()
try {
formatable.format(message)
} finally {
formatable.close()
}
} else {
throw IOException("Tag unterstützt kein NDEF-Schreiben")
}
}
fun writeUltralight(tag: Tag, pages: List<ByteArray>) {
val ul = MifareUltralight.get(tag) ?: throw IOException("Kein Ultralight-Tag")
ul.connect()
try {
pages.forEachIndexed { i, page ->
ul.writePage(i, page)
}
} finally {
ul.close()
}
}
fun writeNfcV(tag: Tag, blocks: List<ByteArray>) {
val nfcV = NfcV.get(tag) ?: throw IOException("Kein NFC-V Tag")
nfcV.connect()
try {
blocks.forEachIndexed { blockNum, data ->
// WRITE_SINGLE_BLOCK: Flags=0x02, Cmd=0x21, BlockNum, Data
val cmd = byteArrayOf(0x02, 0x21, blockNum.toByte()) + data
val response = nfcV.transceive(cmd)
if (response.isEmpty() || (response[0].toInt() and 0x01) != 0) {
val errCode = if (response.size >= 2) "%02X".format(response[1]) else "?"
throw IOException("Block $blockNum Fehler (ISO 15693 Error $errCode)")
}
}
} finally {
try { nfcV.close() } catch (_: Exception) {}
}
}
fun detectWriteMethod(tag: Tag): WriteMethod {
val techs = tag.techList.toList()
return when {
techs.contains(NfcV::class.java.name) -> WriteMethod.NFC_V
techs.contains(MifareUltralight::class.java.name) -> WriteMethod.ULTRALIGHT
techs.contains(MifareClassic::class.java.name) -> WriteMethod.MIFARE_CLASSIC
techs.contains(Ndef::class.java.name) -> WriteMethod.NDEF
techs.contains(NdefFormatable::class.java.name) -> WriteMethod.NDEF_FORMATABLE
else -> WriteMethod.UNSUPPORTED
}
}
enum class WriteMethod { NDEF, NDEF_FORMATABLE, ULTRALIGHT, NFC_V, MIFARE_CLASSIC, UNSUPPORTED }
fun buildTextNdef(text: String, locale: String = "de"): NdefMessage {
val langBytes = locale.toByteArray(Charsets.US_ASCII)
val textBytes = text.toByteArray(Charsets.UTF_8)
val payload = ByteArray(1 + langBytes.size + textBytes.size)
payload[0] = langBytes.size.toByte()
langBytes.copyInto(payload, 1)
textBytes.copyInto(payload, 1 + langBytes.size)
val record = NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, ByteArray(0), payload)
return NdefMessage(arrayOf(record))
}
fun buildUriNdef(uri: String): NdefMessage {
val record = NdefRecord.createUri(android.net.Uri.parse(uri))
return NdefMessage(arrayOf(record))
}
fun buildRawNdef(hexPayload: String): NdefMessage {
val bytes = hexPayload.replace(" ", "").replace(":", "")
.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val record = NdefRecord(NdefRecord.TNF_UNKNOWN, ByteArray(0), ByteArray(0), bytes)
return NdefMessage(arrayOf(record))
}
private fun detectTagType(techList: List<String>): String = when {
techList.contains(MifareUltralight::class.java.name) -> "MIFARE Ultralight / NTAG"
techList.contains(MifareClassic::class.java.name) -> "MIFARE Classic"
techList.contains(IsoDep::class.java.name) -> "ISO-DEP (ISO 14443-4)"
techList.contains(NfcF::class.java.name) -> "NFC-F (FeliCa)"
techList.contains(NfcV::class.java.name) -> "NFC-V (ISO 15693)"
techList.contains(Ndef::class.java.name) -> "NDEF"
techList.contains(NfcA::class.java.name) -> "NFC-A (ISO 14443-3A)"
techList.contains(NfcB::class.java.name) -> "NFC-B (ISO 14443-3B)"
else -> "Unbekannt"
}
}
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/card_dark" />
<corners android:radius="12dp" />
<stroke android:width="1dp" android:color="@color/divider" />
</shape>
</item>
<!-- Left accent border -->
<item android:right="99999dp">
<shape android:shape="rectangle">
<solid android:color="@color/accent_cyan" />
<corners android:topLeftRadius="12dp" android:bottomLeftRadius="12dp" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/accent_cyan_alpha10" />
<stroke
android:width="1.5dp"
android:color="@color/accent_cyan_alpha20" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/error_alpha20" />
<stroke android:width="1dp" android:color="@color/error_red" />
<corners android:radius="12dp" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/success_alpha20" />
<stroke android:width="1dp" android:color="@color/success" />
<corners android:radius="12dp" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/accent_cyan_alpha10" />
<stroke android:width="1dp" android:color="@color/accent_cyan_alpha20" />
<corners android:radius="12dp" />
</shape>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/success" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/error_red" />
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Center dot -->
<path
android:fillColor="#00D1FF"
android:pathData="M22,24 A2,2 0 1,1 26,24 A2,2 0 1,1 22,24" />
<!-- Arc 1 - inner -->
<path
android:strokeColor="#00D1FF"
android:strokeWidth="2.5"
android:fillColor="@android:color/transparent"
android:strokeLineCap="round"
android:pathData="M28,17.5 A8.5,8.5 0 0,1 28,30.5" />
<!-- Arc 2 - middle -->
<path
android:strokeColor="#00D1FF"
android:strokeWidth="2.5"
android:fillColor="@android:color/transparent"
android:strokeLineCap="round"
android:strokeAlpha="0.7"
android:pathData="M32,13 A13,13 0 0,1 32,35" />
<!-- Arc 3 - outer -->
<path
android:strokeColor="#00D1FF"
android:strokeWidth="2.5"
android:fillColor="@android:color/transparent"
android:strokeLineCap="round"
android:strokeAlpha="0.4"
android:pathData="M36,8.5 A17.5,17.5 0 0,1 36,39.5" />
</vector>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke
android:width="2dp"
android:color="@color/accent_cyan" />
</shape>
+124
View File
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_dark"
android:fitsSystemWindows="true"
app:elevation="0dp">
<!-- Brand Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<!-- NFC Icon -->
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/ic_nfc"
android:contentDescription="NFC" />
<!-- Brand Text -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name_brand"
android:textColor="@color/accent_cyan"
android:textSize="22sp"
android:textStyle="bold"
android:letterSpacing="0.05" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name_sub"
android:textColor="@color/text_secondary"
android:textSize="10sp"
android:letterSpacing="0.2" />
</LinearLayout>
<!-- NFC Status Dot -->
<LinearLayout
android:id="@+id/nfcStatusBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/bg_status_waiting"
android:paddingHorizontal="10dp"
android:paddingVertical="5dp">
<View
android:id="@+id/nfcStatusDot"
android:layout_width="7dp"
android:layout_height="7dp"
android:background="@drawable/dot_active" />
<TextView
android:id="@+id/tvNfcStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="NFC"
android:textColor="@color/accent_cyan"
android:textSize="11sp"
android:textStyle="bold"
android:letterSpacing="0.1" />
</LinearLayout>
</LinearLayout>
<!-- Divider line under header -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider" />
<!-- Tabs -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/surface_dark"
app:tabMode="fixed"
app:tabGravity="fill"
app:tabTextColor="@color/text_secondary"
app:tabSelectedTextColor="@color/accent_cyan"
app:tabIndicatorColor="@color/accent_cyan"
app:tabIndicatorHeight="2dp"
app:tabIndicatorFullWidth="false"
app:tabTextAppearance="@style/JrzTabText" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="20dp"
android:paddingBottom="24dp">
<!-- Step 1 Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardStep1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/accent_cyan_alpha20"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Step Label -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="28dp"
android:layout_height="28dp"
android:text="1"
android:textColor="@color/bg_dark"
android:textSize="13sp"
android:textStyle="bold"
android:textAlignment="center"
android:gravity="center"
android:background="@drawable/bg_nfc_circle" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:text="@string/clone_step1"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvSourceStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/clone_no_data"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<!-- Source Details (hidden until read) -->
<LinearLayout
android:id="@+id/layoutSourceDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="@drawable/bg_status_waiting"
android:visibility="gone">
<TextView
android:id="@+id/tvSourceUid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textColor="@color/accent_cyan"
android:textSize="12sp" />
<TextView
android:id="@+id/tvSourceType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/tvSourceSize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="monospace"
android:textColor="@color/text_mono"
android:textSize="12sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnReadSource"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="@string/clone_read_source_btn"
android:textColor="@color/bg_dark"
android:textStyle="bold"
app:backgroundTint="@color/accent_cyan"
app:cornerRadius="10dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Connector Arrow -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="32dp"
android:orientation="horizontal"
android:gravity="center">
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/divider" />
</LinearLayout>
<!-- Step 2 Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardStep2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Step Label -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="28dp"
android:layout_height="28dp"
android:text="2"
android:textColor="@color/text_secondary"
android:textSize="13sp"
android:textStyle="bold"
android:textAlignment="center"
android:gravity="center"
android:background="@drawable/ring_pulse" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:text="@string/clone_step2"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tvTargetStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textColor="@color/text_secondary"
android:textSize="13sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnWriteTarget"
android:layout_width="match_parent"
android:layout_height="48dp"
android:enabled="false"
android:text="@string/clone_write_target_btn"
android:textStyle="bold"
app:cornerRadius="10dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- UID Warning -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/warning_alpha20"
app:cardCornerRadius="12dp"
app:strokeColor="@color/warning_orange"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/warning_orange"
android:textSize="12sp"
android:lineSpacingExtra="2dp"
android:text="UID wird NICHT geklont. UID-basierte Zugangssysteme erkennen den Klon nicht." />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
+230
View File
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="20dp"
android:paddingBottom="24dp">
<!-- Status Bar -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/hceStatusCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="14dp"
android:gravity="center_vertical">
<View
android:id="@+id/hceStatusDot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@drawable/dot_inactive" />
<TextView
android:id="@+id/tvHceStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:text="@string/hce_status_inactive"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HCE"
android:textColor="@color/text_secondary"
android:textSize="11sp"
android:letterSpacing="0.15" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Config Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="KONFIGURATION"
style="@style/JrzSectionLabel"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAid"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/hce_aid_hint"
app:helperText="Standard: D2760000850101 (NDEF)"
app:boxStrokeColor="@color/accent_cyan_alpha20"
app:hintTextColor="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textColor="@color/accent_cyan"
android:inputType="textCapCharacters|text"
android:text="D2760000850101" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPayload"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hce_payload_hint"
app:boxStrokeColor="@color/accent_cyan_alpha20"
app:hintTextColor="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPayload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="2"
android:gravity="top"
android:textColor="@color/text_primary"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Buttons -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnEnableHce"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginBottom="8dp"
android:text="@string/hce_enable_btn"
android:textColor="@color/bg_dark"
android:textStyle="bold"
android:letterSpacing="0.08"
app:backgroundTint="@color/accent_cyan"
app:cornerRadius="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDisableHce"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="20dp"
android:text="@string/hce_disable_btn"
android:textColor="@color/text_secondary"
android:enabled="false"
app:strokeColor="@color/divider"
app:cornerRadius="12dp" />
<!-- Warning -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/warning_alpha20"
app:cardCornerRadius="12dp"
app:strokeColor="@color/warning_orange"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textColor="@color/warning_orange"
android:textSize="12sp"
android:lineSpacingExtra="2dp"
android:text="@string/hce_warning" />
</com.google.android.material.card.MaterialCardView>
<!-- APDU Log -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="APDU-LOG"
style="@style/JrzSectionLabel"
android:textColor="@color/accent_cyan" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearLog"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Leeren"
android:textColor="@color/text_secondary"
android:textSize="11sp" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardApduLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/log_background"
app:cardCornerRadius="12dp"
app:strokeColor="@color/accent_cyan_alpha10"
app:strokeWidth="1dp">
<TextView
android:id="@+id/tvApduLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp"
android:fontFamily="monospace"
android:textColor="@color/log_text"
android:textSize="11sp"
android:lineSpacingExtra="3dp"
android:minLines="6"
android:text="Kein APDU-Verkehr…" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
+291
View File
@@ -0,0 +1,291 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="20dp"
android:paddingBottom="24dp">
<!-- NFC Scan Area -->
<FrameLayout
android:id="@+id/scanArea"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp">
<!-- Pulse rings (animated in code) -->
<View
android:id="@+id/ring3"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center"
android:background="@drawable/ring_pulse"
android:alpha="0" />
<View
android:id="@+id/ring2"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:background="@drawable/ring_pulse"
android:alpha="0" />
<View
android:id="@+id/ring1"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:background="@drawable/ring_pulse"
android:alpha="0" />
<!-- Center circle -->
<FrameLayout
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:background="@drawable/bg_nfc_circle">
<ImageView
android:id="@+id/ivNfcIcon"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_gravity="center"
android:src="@drawable/ic_nfc"
android:contentDescription="NFC" />
</FrameLayout>
</FrameLayout>
<!-- Status Text -->
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/read_instruction"
android:textAlignment="center"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:letterSpacing="0.04" />
<!-- UID Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardUid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/accent_cyan_alpha20"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="UID"
style="@style/JrzSectionLabel" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="6dp">
<TextView
android:id="@+id/tvUid"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="monospace"
android:textColor="@color/accent_cyan"
android:textSize="17sp"
android:textStyle="bold"
android:letterSpacing="0.05" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCopyUid"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/read_copy_uid"
app:icon="@android:drawable/ic_menu_share"
app:iconTint="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Tag Type Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardTagType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TAG-TYP"
style="@style/JrzSectionLabel" />
<TextView
android:id="@+id/tvTagType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:background="@color/divider" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TECHNOLOGIEN"
style="@style/JrzSectionLabel" />
<TextView
android:id="@+id/tvTechList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="monospace"
android:textColor="@color/text_mono"
android:textSize="12sp"
android:lineSpacingExtra="4dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- NDEF Content Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardNdef"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="NDEF-INHALT"
style="@style/JrzSectionLabel" />
<TextView
android:id="@+id/tvNdefContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="monospace"
android:textColor="@color/text_mono"
android:textSize="13sp"
android:lineSpacingExtra="3dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Raw / Diagnose Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardRaw"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:cardBackgroundColor="@color/log_background"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ROHDATEN / DIAGNOSE"
style="@style/JrzSectionLabel"
android:textColor="@color/accent_cyan" />
<TextView
android:id="@+id/tvRawPages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="monospace"
android:textColor="@color/log_text"
android:textSize="11sp"
android:lineSpacingExtra="2dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Save Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSaveDump"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="8dp"
android:text="@string/read_save_btn"
android:textColor="@color/bg_dark"
android:textStyle="bold"
android:letterSpacing="0.08"
android:visibility="gone"
app:backgroundTint="@color/accent_cyan"
app:cornerRadius="12dp" />
</LinearLayout>
</ScrollView>
+183
View File
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dark"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="20dp"
android:paddingBottom="24dp">
<!-- Header -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="NACHRICHTENTYP"
style="@style/JrzSectionLabel"
android:layout_marginBottom="10dp" />
<!-- Type Chips -->
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipText"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text (de)"
android:checked="true"
app:checkedIconVisible="false" />
<com.google.android.material.chip.Chip
android:id="@+id/chipUrl"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="URL"
app:checkedIconVisible="false" />
<com.google.android.material.chip.Chip
android:id="@+id/chipHex"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Roh-Hex"
app:checkedIconVisible="false" />
</com.google.android.material.chip.ChipGroup>
<!-- Input Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/divider"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilContent"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/write_content_hint"
app:boxStrokeColor="@color/accent_cyan_alpha20"
app:hintTextColor="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:gravity="top"
android:textColor="@color/text_primary"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvByteCount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="0 Bytes"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:fontFamily="monospace" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Write Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnWrite"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="8dp"
android:text="@string/write_btn"
android:textColor="@color/bg_dark"
android:textStyle="bold"
android:letterSpacing="0.08"
app:backgroundTint="@color/accent_cyan"
app:cornerRadius="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelWrite"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:text="@string/write_cancel_btn"
android:textColor="@color/text_secondary"
android:visibility="gone"
app:strokeColor="@color/divider"
app:cornerRadius="12dp" />
<!-- Status Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/writeStatusCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
app:cardBackgroundColor="@color/card_dark"
app:cardCornerRadius="12dp"
app:strokeColor="@color/accent_cyan_alpha20"
app:strokeWidth="1dp">
<TextView
android:id="@+id/tvWriteStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAlignment="center"
android:textColor="@color/text_primary"
android:textSize="14sp" />
</com.google.android.material.card.MaterialCardView>
<!-- Warning Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardFormatWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:visibility="gone"
app:cardBackgroundColor="@color/warning_alpha20"
app:cardCornerRadius="12dp"
app:strokeColor="@color/warning_orange"
app:strokeWidth="1dp">
<TextView
android:id="@+id/tvFormatWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp"
android:textColor="@color/warning_orange"
android:textSize="13sp" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" />
<foreground android:drawable="@drawable/ic_launcher_fg" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" />
<foreground android:drawable="@drawable/ic_launcher_fg" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Brand / Background -->
<color name="bg_dark">#08091A</color>
<color name="surface_dark">#0D1128</color>
<color name="card_dark">#111827</color>
<color name="card_elevated">#162035</color>
<color name="divider">#1E2D40</color>
<!-- Accent -->
<color name="accent_cyan">#00D1FF</color>
<color name="accent_cyan_alpha20">#3300D1FF</color>
<color name="accent_cyan_alpha10">#1A00D1FF</color>
<color name="accent_purple">#7C3AED</color>
<color name="accent_purple_alpha20">#337C3AED</color>
<!-- Text -->
<color name="text_primary">#E2E8F0</color>
<color name="text_secondary">#64748B</color>
<color name="text_mono">#94A3B8</color>
<!-- Status -->
<color name="success">#10B981</color>
<color name="success_alpha20">#3310B981</color>
<color name="error_red">#EF4444</color>
<color name="error_alpha20">#33EF4444</color>
<color name="warning_orange">#F59E0B</color>
<color name="warning_alpha20">#33F59E0B</color>
<!-- Terminal / Log -->
<color name="log_background">#050812</color>
<color name="log_text">#00D1FF</color>
<!-- Legacy (still used in code) -->
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="success_green">#10B981</color>
</resources>
+65
View File
@@ -0,0 +1,65 @@
<resources>
<string name="app_name">JRZ NFC Cloner</string>
<string name="app_name_brand">JRZ</string>
<string name="app_name_sub">NFC CLONER</string>
<string name="tab_read">Lesen</string>
<string name="tab_write">Schreiben</string>
<string name="tab_clone">Klonen</string>
<string name="tab_hce">HCE</string>
<string name="hce_service_description">NFC HCE Emulation</string>
<string name="hce_aid_group_description">NFC Tag Emulation</string>
<!-- Read Fragment -->
<string name="read_instruction">Halte einen NFC-Tag ans Telefon</string>
<string name="read_uid">UID</string>
<string name="read_tag_type">Tag-Typ</string>
<string name="read_technologies">Technologien</string>
<string name="read_ndef_content">NDEF-Inhalt</string>
<string name="read_raw_pages">Rohdaten (Seiten)</string>
<string name="read_save_btn">Tag-Dump speichern</string>
<string name="read_copy_uid">UID kopieren</string>
<!-- Write Fragment -->
<string name="write_instruction">NDEF-Nachricht eingeben, dann Tag halten</string>
<string name="write_type_text">Text (de)</string>
<string name="write_type_url">URL</string>
<string name="write_type_hex">Roh-Hex</string>
<string name="write_content_hint">Inhalt eingeben…</string>
<string name="write_btn">Schreiben starten</string>
<string name="write_cancel_btn">Abbrechen</string>
<string name="write_waiting">Warte auf Tag…</string>
<string name="write_success">Erfolgreich geschrieben!</string>
<string name="write_readonly_error">Tag ist schreibgeschützt</string>
<string name="write_size_error">Inhalt zu groß für diesen Tag</string>
<!-- Clone Fragment -->
<string name="clone_step1">Schritt 1: Quell-Tag lesen</string>
<string name="clone_step2">Schritt 2: Ziel-Tag beschreiben</string>
<string name="clone_read_source_btn">Quelle scannen</string>
<string name="clone_write_target_btn">Auf Ziel schreiben</string>
<string name="clone_no_data">Noch kein Quell-Tag gelesen</string>
<string name="clone_source_ready">Quelle bereit %1$d Bytes</string>
<string name="clone_waiting_source">Halte Quell-Tag ans Telefon…</string>
<string name="clone_waiting_target">Halte Ziel-Tag ans Telefon…</string>
<string name="clone_success">Klon erfolgreich!</string>
<!-- HCE Fragment -->
<string name="hce_instruction">Emuliert einen NDEF-Tag via HCE.\nFunktioniert nur bei Systemen die AIDs prüfen, NICHT bei UID-basierter Prüfung.</string>
<string name="hce_aid_hint">AID (Hex, z.B. D2760000850101)</string>
<string name="hce_payload_hint">NDEF-Payload (Text oder Hex)</string>
<string name="hce_enable_btn">HCE aktivieren</string>
<string name="hce_disable_btn">HCE deaktivieren</string>
<string name="hce_status_active">HCE aktiv</string>
<string name="hce_status_inactive">HCE inaktiv</string>
<string name="hce_apdu_log">APDU-Log</string>
<string name="hce_warning">Hinweis: UID-basierte Zugangssysteme können nicht per HCE emuliert werden.</string>
<!-- Errors / Status -->
<string name="nfc_not_supported">NFC wird auf diesem Gerät nicht unterstützt</string>
<string name="nfc_disabled">NFC ist deaktiviert. Bitte in den Einstellungen aktivieren.</string>
<string name="error_connect">Verbindung zum Tag fehlgeschlagen</string>
<string name="error_read">Lesefehler: %1$s</string>
<string name="error_write">Schreibfehler: %1$s</string>
<string name="uid_copied">UID in Zwischenablage kopiert</string>
<string name="dump_saved">Dump gespeichert: %1$s</string>
</resources>
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.NFCApp" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/accent_cyan</item>
<item name="colorPrimaryContainer">@color/card_elevated</item>
<item name="colorOnPrimaryContainer">@color/accent_cyan</item>
<item name="colorSecondary">@color/accent_purple</item>
<item name="colorSecondaryContainer">@color/accent_purple_alpha20</item>
<item name="colorSurface">@color/surface_dark</item>
<item name="colorSurfaceVariant">@color/card_dark</item>
<item name="android:colorBackground">@color/bg_dark</item>
<item name="colorOnSurface">@color/text_primary</item>
<item name="colorOnSurfaceVariant">@color/text_secondary</item>
<item name="colorOutline">@color/divider</item>
<!-- Edge-to-edge: transparent bars -->
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<!-- Tab indicator color -->
<item name="tabIndicatorColor">@color/accent_cyan</item>
<!-- Card style -->
<item name="materialCardViewStyle">@style/JrzCardView</item>
<!-- Chip style override -->
<item name="chipStyle">@style/JrzChip</item>
</style>
<style name="JrzCardView" parent="Widget.Material3.CardView.Elevated">
<item name="cardBackgroundColor">@color/card_dark</item>
<item name="cardCornerRadius">12dp</item>
<item name="cardElevation">0dp</item>
<item name="strokeColor">@color/divider</item>
<item name="strokeWidth">1dp</item>
</style>
<style name="JrzChip" parent="Widget.Material3.Chip.Filter">
<item name="android:textColor">@color/text_secondary</item>
<item name="checkedIconVisible">false</item>
</style>
<style name="JrzSectionLabel" parent="TextAppearance.Material3.LabelSmall">
<item name="android:textColor">@color/text_secondary</item>
<item name="android:textAllCaps">true</item>
<item name="android:letterSpacing">0.12</item>
<item name="android:textSize">11sp</item>
</style>
<style name="JrzMonoValue" parent="TextAppearance.Material3.BodyMedium">
<item name="android:fontFamily">monospace</item>
<item name="android:textColor">@color/text_primary</item>
<item name="android:textSize">14sp</item>
</style>
<style name="JrzTabText" parent="TextAppearance.Material3.LabelLarge">
<item name="android:textSize">12sp</item>
<item name="android:letterSpacing">0.08</item>
<item name="android:textAllCaps">true</item>
</style>
</resources>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
AID-Gruppen für HCE.
Die Standard-AID hier ist ein Platzhalter (D2760000850101 = NDEF-AID).
Wird zur Laufzeit über HceService dynamisch konfiguriert.
-->
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/hce_service_description"
android:requireDeviceUnlock="false">
<aid-group android:category="other" android:description="@string/hce_aid_group_description">
<!-- NDEF Application AID -->
<aid-filter android:name="D2760000850101" />
</aid-group>
</host-apdu-service>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NdefFormatable</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcB</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcF</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
</tech-list>
</resources>
+4
View File
@@ -0,0 +1,4 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
+24
View File
@@ -0,0 +1,24 @@
[versions]
agp = "8.5.2"
kotlin = "2.0.21"
coreKtx = "1.13.1"
appcompat = "1.7.0"
material = "1.12.0"
constraintlayout = "2.1.4"
lifecycle = "2.8.6"
navigation = "2.8.3"
viewpager2 = "1.1.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+17
View File
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "NFCApp"
include(":app")