diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 368cc91..d8c9a30 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ try { f.addDataType("*/*") } catch (_: IntentFilter.MalformedMimeTypeException) {}
+ }
+ )
+ val techLists = arrayOf(arrayOf(android.nfc.tech.Ndef::class.java.name))
nfcAdapter?.enableForegroundDispatch(
this,
nfcPendingIntent,
- null, // intercept all intent filters
- null // intercept all tech lists
+ intentFilters,
+ techLists
)
}
@@ -174,12 +186,20 @@ class MainActivity : AppCompatActivity() {
}
// NFC tag discovered → hand off to NfcViewModel for reading
- val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
+ val tag = intent.getNfcTag()
if (tag != null) {
nfcVm.onTagDiscovered(tag)
}
}
+ private fun Intent.getNfcTag(): Tag? =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableExtra(NfcAdapter.EXTRA_TAG)
+ }
+
// ── Permission / battery helpers (unchanged) ──────────────────────────────
private fun requestNotificationPermissionIfNeeded() {
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt
index a63406c..7a569cf 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt
@@ -473,8 +473,8 @@ fun WalletScreen(
modifier = Modifier
.fillMaxWidth()
.clickable {
- nfcVm.dismissNfcOffer()
vm.scan(offer.raw)
+ nfcVm.dismissNfcOffer()
navController.navigate(TabItem.Send.route) {
launchSingleTop = true
}
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nfc/NfcViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nfc/NfcViewModel.kt
index 3ed09f8..ead6782 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nfc/NfcViewModel.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nfc/NfcViewModel.kt
@@ -1,22 +1,23 @@
package com.bitcointxoko.gudariwallet.ui.nfc
-import android.content.ComponentName
-import android.content.Context
-import android.nfc.NfcAdapter
+import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NdefRecord
+import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.Ndef
-import android.nfc.tech.NdefFormatable
+import android.os.Build
import android.util.Log
import androidx.lifecycle.ViewModel
-import com.bitcointxoko.gudariwallet.ui.nfc.NdefHceService
+import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
import com.bitcointxoko.gudariwallet.util.SendInputDetector
import com.bitcointxoko.gudariwallet.util.SendInputType
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
private const val TAG = "NfcViewModel"
@@ -62,50 +63,80 @@ class NfcViewModel : ViewModel() {
* recognisable payment URI.
*/
fun onTagDiscovered(tag: Tag) {
- Log.d(TAG, "onTagDiscovered: ${tag.id?.toHex()}")
-
- val text = readNdefText(tag) ?: run {
- Log.w(TAG, "Could not read NDEF from tag")
- return
+ Log.d(TAG, "onTagDiscovered: ${tag.id.joinToString("") { "%02X".format(it) }}")
+ viewModelScope.launch(Dispatchers.IO) { // ← move to IO thread
+ readNdefText(tag)
}
-
- Log.d(TAG, "Tag NDEF text: $text")
- val trimmed = text.trim()
- val type = SendInputDetector.detect(trimmed)
- if (type == SendInputType.Unknown) {
- Log.d(TAG, "Tag content not a recognised payment URI")
- return
- }
-
- _nfcOfferState.value = NfcOfferState.Detected(raw = trimmed, inputType = type)
}
+ /** Cold-start path: Android already parsed the NDEF message into the intent.
+ * No tag connection needed — avoids "Tag is out of date" SecurityException. */
+ fun onNfcIntent(intent: Intent) {
+ if (intent.action != NfcAdapter.ACTION_NDEF_DISCOVERED &&
+ intent.action != NfcAdapter.ACTION_TAG_DISCOVERED) return
+
+ // Android attaches the parsed NDEF messages directly to the intent
+ val rawMessages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, NdefMessage::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
+ }
+
+ if (rawMessages != null && rawMessages.isNotEmpty()) {
+ val message = rawMessages[0] as NdefMessage
+ val uri = extractUri(message)
+ if (uri != null) {
+ Log.d(TAG, "onNfcIntent (cold-start): uri=$uri")
+ _nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
+ return
+ }
+ }
+
+ // Fallback: no NDEF messages in intent, try connecting to tag
+ val tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
+ }
+ if (tag != null) {
+ viewModelScope.launch(Dispatchers.IO) {
+ readNdefText(tag)
+ }
+ }
+ }
+
+
fun dismissNfcOffer() {
_nfcOfferState.value = NfcOfferState.None
}
// ── NDEF reading ─────────────────────────────────────────────────────────
- private fun readNdefText(tag: Tag): String? {
- // Try standard NDEF first (covers Type 1–5 tags, NTAG21x, etc.)
- Ndef.get(tag)?.use { ndef ->
- ndef.connect()
- val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return null
- return extractUri(msg)
+ private fun readNdefText(tag: Tag) {
+ val uri = try {
+ Ndef.get(tag)?.use { ndef ->
+ ndef.connect()
+ val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return
+ extractUri(msg)
+ } ?: IsoDep.get(tag)?.use { isoDep ->
+ isoDep.connect()
+ isoDep.timeout = 3000
+ val msg = readNdefFromIsoDep(isoDep) ?: return
+ extractUri(msg)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "readNdefText error: ${e.message}")
+ null
}
-
- // Try NdefFormatable (blank tags — nothing to read)
- // Try IsoDep last (Type 4 / ISO-DEP tags like Bolt Cards)
- IsoDep.get(tag)?.use { isoDep ->
- isoDep.connect()
- isoDep.timeout = 3000
- val msg = readNdefFromIsoDep(isoDep) ?: return null
- return extractUri(msg)
+ if (uri != null) {
+ Log.d(TAG, "readNdefText: uri=$uri")
+ _nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
+ } else {
+ Log.w(TAG, "readNdefText: no URI found in tag")
}
-
- return null
}
-
/**
* Reads an NDEF message from an ISO-DEP (Type 4) tag by replaying the
* same APDU sequence our HCE service responds to.