feat: cold start on scanning nfc tag
This commit is contained in:
@@ -44,6 +44,26 @@
|
|||||||
<data android:scheme="lnurlc" />
|
<data android:scheme="lnurlc" />
|
||||||
<data android:scheme="lnurl" />
|
<data android:scheme="lnurl" />
|
||||||
</intent-filter>
|
</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>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.nfc.NfcAdapter
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.Tag
|
import android.nfc.Tag
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -54,8 +55,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private val nfcPendingIntent: PendingIntent by lazy {
|
private val nfcPendingIntent: PendingIntent by lazy {
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this, 0,
|
this, 0,
|
||||||
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
Intent(this, javaClass).apply {
|
||||||
PendingIntent.FLAG_MUTABLE
|
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)
|
// Initialise NFC adapter (null on devices without NFC — handled gracefully)
|
||||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||||
|
|
||||||
|
nfcVm.onNfcIntent(intent)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
@@ -137,13 +144,18 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
// Give this activity first pick of all NFC tag intents while foregrounded.
|
// Restrict to NDEF only — avoids TAG_DISCOVERED BAL blocks for non-NDEF tags
|
||||||
// Passing null for techLists and intentFilters means we intercept every tag type.
|
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(
|
nfcAdapter?.enableForegroundDispatch(
|
||||||
this,
|
this,
|
||||||
nfcPendingIntent,
|
nfcPendingIntent,
|
||||||
null, // intercept all intent filters
|
intentFilters,
|
||||||
null // intercept all tech lists
|
techLists
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,12 +186,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NFC tag discovered → hand off to NfcViewModel for reading
|
// NFC tag discovered → hand off to NfcViewModel for reading
|
||||||
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
val tag = intent.getNfcTag()
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
nfcVm.onTagDiscovered(tag)
|
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) ──────────────────────────────
|
// ── Permission / battery helpers (unchanged) ──────────────────────────────
|
||||||
|
|
||||||
private fun requestNotificationPermissionIfNeeded() {
|
private fun requestNotificationPermissionIfNeeded() {
|
||||||
|
|||||||
@@ -473,8 +473,8 @@ fun WalletScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
nfcVm.dismissNfcOffer()
|
|
||||||
vm.scan(offer.raw)
|
vm.scan(offer.raw)
|
||||||
|
nfcVm.dismissNfcOffer()
|
||||||
navController.navigate(TabItem.Send.route) {
|
navController.navigate(TabItem.Send.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui.nfc
|
package com.bitcointxoko.gudariwallet.ui.nfc
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.Intent
|
||||||
import android.content.Context
|
|
||||||
import android.nfc.NfcAdapter
|
|
||||||
import android.nfc.NdefMessage
|
import android.nfc.NdefMessage
|
||||||
import android.nfc.NdefRecord
|
import android.nfc.NdefRecord
|
||||||
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.Tag
|
import android.nfc.Tag
|
||||||
import android.nfc.tech.IsoDep
|
import android.nfc.tech.IsoDep
|
||||||
import android.nfc.tech.Ndef
|
import android.nfc.tech.Ndef
|
||||||
import android.nfc.tech.NdefFormatable
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
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.ui.NfcOfferState
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputType
|
import com.bitcointxoko.gudariwallet.util.SendInputType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val TAG = "NfcViewModel"
|
private const val TAG = "NfcViewModel"
|
||||||
|
|
||||||
@@ -62,50 +63,80 @@ class NfcViewModel : ViewModel() {
|
|||||||
* recognisable payment URI.
|
* recognisable payment URI.
|
||||||
*/
|
*/
|
||||||
fun onTagDiscovered(tag: Tag) {
|
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
|
||||||
val text = readNdefText(tag) ?: run {
|
readNdefText(tag)
|
||||||
Log.w(TAG, "Could not read NDEF from tag")
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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<Tag>(NfcAdapter.EXTRA_TAG)
|
||||||
|
}
|
||||||
|
if (tag != null) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
readNdefText(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun dismissNfcOffer() {
|
fun dismissNfcOffer() {
|
||||||
_nfcOfferState.value = NfcOfferState.None
|
_nfcOfferState.value = NfcOfferState.None
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── NDEF reading ─────────────────────────────────────────────────────────
|
// ── NDEF reading ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun readNdefText(tag: Tag): String? {
|
private fun readNdefText(tag: Tag) {
|
||||||
// Try standard NDEF first (covers Type 1–5 tags, NTAG21x, etc.)
|
val uri = try {
|
||||||
Ndef.get(tag)?.use { ndef ->
|
Ndef.get(tag)?.use { ndef ->
|
||||||
ndef.connect()
|
ndef.connect()
|
||||||
val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return null
|
val msg = ndef.ndefMessage ?: ndef.cachedNdefMessage ?: return
|
||||||
return extractUri(msg)
|
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
|
||||||
}
|
}
|
||||||
|
if (uri != null) {
|
||||||
// Try NdefFormatable (blank tags — nothing to read)
|
Log.d(TAG, "readNdefText: uri=$uri")
|
||||||
// Try IsoDep last (Type 4 / ISO-DEP tags like Bolt Cards)
|
_nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
|
||||||
IsoDep.get(tag)?.use { isoDep ->
|
} else {
|
||||||
isoDep.connect()
|
Log.w(TAG, "readNdefText: no URI found in tag")
|
||||||
isoDep.timeout = 3000
|
|
||||||
val msg = readNdefFromIsoDep(isoDep) ?: return null
|
|
||||||
return extractUri(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads an NDEF message from an ISO-DEP (Type 4) tag by replaying the
|
* Reads an NDEF message from an ISO-DEP (Type 4) tag by replaying the
|
||||||
* same APDU sequence our HCE service responds to.
|
* same APDU sequence our HCE service responds to.
|
||||||
|
|||||||
Reference in New Issue
Block a user