feat: cold start on scanning nfc tag
This commit is contained in:
@@ -44,6 +44,26 @@
|
||||
<data android:scheme="lnurlc" />
|
||||
<data android:scheme="lnurl" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="lightning" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="lnurl" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="bitcoin" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.os.Build
|
||||
@@ -54,8 +55,12 @@ class MainActivity : AppCompatActivity() {
|
||||
private val nfcPendingIntent: PendingIntent by lazy {
|
||||
PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
Intent(this, javaClass).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
// Do NOT add FLAG_ACTIVITY_NEW_TASK — this causes
|
||||
// balDontBringExistingBackgroundTaskStackToFg = true
|
||||
},
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,6 +107,8 @@ class MainActivity : AppCompatActivity() {
|
||||
// Initialise NFC adapter (null on devices without NFC — handled gracefully)
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
|
||||
nfcVm.onNfcIntent(intent)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
@@ -137,13 +144,18 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Give this activity first pick of all NFC tag intents while foregrounded.
|
||||
// Passing null for techLists and intentFilters means we intercept every tag type.
|
||||
// Restrict to NDEF only — avoids TAG_DISCOVERED BAL blocks for non-NDEF tags
|
||||
val intentFilters = arrayOf(
|
||||
IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).also { f ->
|
||||
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<Tag>(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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,23 +63,50 @@ class NfcViewModel : ViewModel() {
|
||||
* recognisable payment URI.
|
||||
*/
|
||||
fun onTagDiscovered(tag: Tag) {
|
||||
Log.d(TAG, "onTagDiscovered: ${tag.id?.toHex()}")
|
||||
Log.d(TAG, "onTagDiscovered: ${tag.id.joinToString("") { "%02X".format(it) }}")
|
||||
viewModelScope.launch(Dispatchers.IO) { // ← move to IO thread
|
||||
readNdefText(tag)
|
||||
}
|
||||
}
|
||||
|
||||
val text = readNdefText(tag) ?: run {
|
||||
Log.w(TAG, "Could not read NDEF from tag")
|
||||
/** 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
|
||||
}
|
||||
|
||||
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)
|
||||
// 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<Tag>(NfcAdapter.EXTRA_TAG)
|
||||
}
|
||||
if (tag != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
readNdefText(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun dismissNfcOffer() {
|
||||
_nfcOfferState.value = NfcOfferState.None
|
||||
@@ -86,26 +114,29 @@ class NfcViewModel : ViewModel() {
|
||||
|
||||
// ── NDEF reading ─────────────────────────────────────────────────────────
|
||||
|
||||
private fun readNdefText(tag: Tag): String? {
|
||||
// Try standard NDEF first (covers Type 1–5 tags, NTAG21x, etc.)
|
||||
private fun readNdefText(tag: Tag) {
|
||||
val uri = try {
|
||||
Ndef.get(tag)?.use { ndef ->
|
||||
ndef.connect()
|
||||
val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return null
|
||||
return extractUri(msg)
|
||||
}
|
||||
|
||||
// Try NdefFormatable (blank tags — nothing to read)
|
||||
// Try IsoDep last (Type 4 / ISO-DEP tags like Bolt Cards)
|
||||
IsoDep.get(tag)?.use { isoDep ->
|
||||
val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return
|
||||
extractUri(msg)
|
||||
} ?: IsoDep.get(tag)?.use { isoDep ->
|
||||
isoDep.connect()
|
||||
isoDep.timeout = 3000
|
||||
val msg = readNdefFromIsoDep(isoDep) ?: return null
|
||||
return extractUri(msg)
|
||||
val msg = readNdefFromIsoDep(isoDep) ?: return
|
||||
extractUri(msg)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "readNdefText error: ${e.message}")
|
||||
null
|
||||
}
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user