@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
77import android.nfc.NdefMessage
88import android.nfc.NdefRecord
99import android.nfc.NfcAdapter
10+ import android.nfc.cardemulation.CardEmulation
1011import android.nfc.Tag
1112import android.nfc.tech.Ndef
1213import android.nfc.tech.IsoDep
@@ -33,6 +34,9 @@ import androidx.fragment.app.FragmentActivity
3334import androidx.lifecycle.lifecycleScope
3435import androidx.lifecycle.ViewModelProvider
3536import github.aeonbtc.ibiswallet.data.local.SecureStorage
37+ import github.aeonbtc.ibiswallet.nfc.NdefHostApduService
38+ import github.aeonbtc.ibiswallet.nfc.NfcReaderModeRequestRegistry
39+ import github.aeonbtc.ibiswallet.nfc.NfcRuntimeStatus
3640import github.aeonbtc.ibiswallet.ui.IbisWalletApp
3741import github.aeonbtc.ibiswallet.ui.screens.CalculatorScreen
3842import github.aeonbtc.ibiswallet.ui.screens.LockScreen
@@ -93,16 +97,22 @@ class MainActivity : FragmentActivity() {
9397 // NFC peer-to-peer negotiation, which prevents two phones from communicating
9498 // when one uses HCE (broadcasting) and the other reads.
9599 private var nfcAdapter: NfcAdapter ? = null
96- private var nfcReaderRequested = false
97- private var nfcReaderActive = false
100+ private val nfcReaderRequests = NfcReaderModeRequestRegistry ()
101+ private val nfcHceRequests = NfcReaderModeRequestRegistry ()
102+ var isNfcReaderModeActive by mutableStateOf(false )
103+ private set
104+ var isPreferredHceServiceActive by mutableStateOf(false )
105+ private set
98106
99107 /* *
100108 * NFC ReaderCallback — invoked on a background thread when a tag is detected.
101109 * Reads NDEF message from the tag (or HCE peer), extracts a supported send payload,
102110 * and posts it to the ViewModel on the main thread.
103111 */
104112 private val nfcReaderCallback = NfcAdapter .ReaderCallback { tag: Tag ->
113+ NfcRuntimeStatus .markReaderTagDetected()
105114 val sendInput = readSendInputFromTag(tag) ? : return @ReaderCallback
115+ NfcRuntimeStatus .markReaderPayloadReceived()
106116 runOnUiThread {
107117 walletViewModel.setPendingSendInput(sendInput)
108118 }
@@ -129,6 +139,7 @@ class MainActivity : FragmentActivity() {
129139 val isoDep = IsoDep .get(tag) ? : return null
130140
131141 return try {
142+ isoDep.timeout = maxOf(isoDep.timeout, 5_000 )
132143 isoDep.connect()
133144
134145 val selectAidResponse = transceiveSelectAid(isoDep)
@@ -244,40 +255,64 @@ class MainActivity : FragmentActivity() {
244255 private fun ByteArray.toHexString (): String = joinToString(" " ) { " %02X" .format(it) }
245256
246257 /* *
247- * Enable NFC reader mode so this activity can read NFC tags and HCE peers.
248- * Disables P2P negotiation so two phones ( one broadcasting via HCE, one reading)
249- * can communicate. Called when Send/Balance screen becomes visible .
258+ * Request NFC reader mode for a visible screen. Reader mode remains enabled until
259+ * all screen owners release their request, so one screen disposing cannot disable
260+ * another screen that still needs NFC .
250261 */
251- fun enableNfcReaderMode ( ) {
252- nfcReaderRequested = true
262+ fun requestNfcReaderMode ( owner : Any ) {
263+ nfcReaderRequests.request(owner)
253264 activateNfcReaderMode()
254265 }
255266
256267 /* *
257- * Disable NFC reader mode. Called when Send/Balance screen is no longer visible .
268+ * Release a previously registered NFC reader mode request .
258269 */
259- fun disableNfcReaderMode () {
260- nfcReaderRequested = false
261- deactivateNfcReaderMode()
270+ fun releaseNfcReaderMode (owner : Any ) {
271+ nfcReaderRequests.release(owner)
272+ if (! nfcReaderRequests.hasActiveRequests()) {
273+ deactivateNfcReaderMode()
274+ }
275+ }
276+
277+ /* *
278+ * Prefer the Ibis HCE service while a receive screen is visible so Android routes
279+ * incoming NFC taps to this app instead of any competing HCE service.
280+ */
281+ fun requestPreferredHceService (owner : Any ) {
282+ nfcHceRequests.request(owner)
283+ activatePreferredHceService()
284+ }
285+
286+ /* *
287+ * Release a previously registered HCE preference request.
288+ */
289+ fun releasePreferredHceService (owner : Any ) {
290+ nfcHceRequests.release(owner)
291+ if (! nfcHceRequests.hasActiveRequests()) {
292+ deactivatePreferredHceService()
293+ }
262294 }
263295
264296 private fun activateNfcReaderMode () {
265- if (nfcReaderActive ) return
297+ if (isNfcReaderModeActive || ! nfcReaderRequests.hasActiveRequests() ) return
266298
267299 val nfcAvailability = getNfcAvailability(secureStorage.isNfcEnabled())
268300 if (! nfcAvailability.hasHardware || ! nfcAvailability.isAppEnabled) {
269- nfcReaderActive = false
301+ isNfcReaderModeActive = false
302+ NfcRuntimeStatus .setReaderInactive()
270303 return
271304 }
272305 if (! nfcAvailability.isSystemEnabled) {
273- nfcReaderActive = false
306+ isNfcReaderModeActive = false
307+ NfcRuntimeStatus .setReaderInactive()
274308 Log .w(TAG , " NFC reader mode unavailable because system NFC is disabled" )
275309 return
276310 }
277311
278312 val adapter = nfcAdapter
279313 if (adapter == null ) {
280- nfcReaderActive = false
314+ isNfcReaderModeActive = false
315+ NfcRuntimeStatus .setReaderInactive()
281316 Log .w(TAG , " NFC reader mode unavailable because the adapter could not be acquired" )
282317 return
283318 }
@@ -289,19 +324,70 @@ class MainActivity : FragmentActivity() {
289324 NfcAdapter .FLAG_READER_NO_PLATFORM_SOUNDS
290325 try {
291326 adapter.enableReaderMode(this , nfcReaderCallback, flags, null )
292- nfcReaderActive = true
327+ isNfcReaderModeActive = true
328+ NfcRuntimeStatus .setReaderReady()
293329 } catch (e: Exception ) {
294- nfcReaderActive = false
330+ isNfcReaderModeActive = false
331+ NfcRuntimeStatus .setReaderInactive()
295332 Log .w(TAG , " Failed to enable NFC reader mode" , e)
296333 }
297334 }
298335
299336 private fun deactivateNfcReaderMode () {
300- if (! nfcReaderActive ) return
337+ if (! isNfcReaderModeActive ) return
301338 try {
302339 nfcAdapter?.disableReaderMode(this )
303340 } catch (_: Exception ) { }
304- nfcReaderActive = false
341+ isNfcReaderModeActive = false
342+ NfcRuntimeStatus .setReaderInactive()
343+ }
344+
345+ private fun activatePreferredHceService () {
346+ if (isPreferredHceServiceActive || ! nfcHceRequests.hasActiveRequests()) return
347+
348+ val adapter = nfcAdapter
349+ if (adapter == null ) {
350+ isPreferredHceServiceActive = false
351+ Log .w(TAG , " HCE preference unavailable because the NFC adapter could not be acquired" )
352+ return
353+ }
354+
355+ val cardEmulation =
356+ runCatching { CardEmulation .getInstance(adapter) }
357+ .getOrNull()
358+ if (cardEmulation == null ) {
359+ isPreferredHceServiceActive = false
360+ Log .w(TAG , " HCE preference unavailable because CardEmulation is not supported" )
361+ return
362+ }
363+
364+ val didSetPreferred =
365+ runCatching {
366+ cardEmulation.setPreferredService(
367+ this ,
368+ ComponentName (this , NdefHostApduService ::class .java),
369+ )
370+ }.getOrElse { error ->
371+ Log .w(TAG , " Failed to prefer Ibis HCE service" , error)
372+ false
373+ }
374+
375+ isPreferredHceServiceActive = didSetPreferred
376+ if (! didSetPreferred) {
377+ Log .w(TAG , " Android refused to prefer the Ibis HCE service" )
378+ }
379+ }
380+
381+ private fun deactivatePreferredHceService () {
382+ val adapter = nfcAdapter
383+ if (adapter != null ) {
384+ runCatching {
385+ CardEmulation .getInstance(adapter).unsetPreferredService(this )
386+ }.onFailure { error ->
387+ Log .w(TAG , " Failed to clear preferred Ibis HCE service" , error)
388+ }
389+ }
390+ isPreferredHceServiceActive = false
305391 }
306392
307393 /* *
@@ -488,12 +574,7 @@ class MainActivity : FragmentActivity() {
488574 showBiometricPrompt(isDuressWithBiometric)
489575 },
490576 isBiometricAvailable = isBiometricAvailable,
491- storedPinLength =
492- if (isDuressWithBiometric) {
493- secureStorage.getDuressPinLength()
494- } else {
495- secureStorage.getStoredPinLength()
496- },
577+ randomizePinPad = secureStorage.getRandomizePinPad(),
497578 isDuressWithBiometric = isDuressWithBiometric,
498579 )
499580 }
@@ -505,11 +586,14 @@ class MainActivity : FragmentActivity() {
505586 override fun onResume () {
506587 super .onResume()
507588
508- // Re-enable NFC reader mode if the Send/Balance screen requested it
509- // (Android requires disabling in onPause and re-enabling in onResume)
510- if (nfcReaderRequested ) {
589+ // Re-enable NFC reader mode if a visible screen still owns a request
590+ // (Android requires disabling in onPause and re-enabling in onResume).
591+ if (nfcReaderRequests.hasActiveRequests() ) {
511592 activateNfcReaderMode()
512593 }
594+ if (nfcHceRequests.hasActiveRequests()) {
595+ activatePreferredHceService()
596+ }
513597
514598 val securityMethod = secureStorage.getSecurityMethod()
515599 if (securityMethod == SecureStorage .SecurityMethod .NONE && ! isCloakActive) {
@@ -559,6 +643,7 @@ class MainActivity : FragmentActivity() {
559643 override fun onPause () {
560644 super .onPause()
561645 deactivateNfcReaderMode()
646+ deactivatePreferredHceService()
562647 }
563648
564649 override fun onStop () {
0 commit comments