Skip to content

Commit ba3ffa4

Browse files
authored
Add files via upload
1 parent 89a04f4 commit ba3ffa4

30 files changed

Lines changed: 2455 additions & 1249 deletions

app/src/main/java/github/aeonbtc/ibiswallet/MainActivity.kt

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
77
import android.nfc.NdefMessage
88
import android.nfc.NdefRecord
99
import android.nfc.NfcAdapter
10+
import android.nfc.cardemulation.CardEmulation
1011
import android.nfc.Tag
1112
import android.nfc.tech.Ndef
1213
import android.nfc.tech.IsoDep
@@ -33,6 +34,9 @@ import androidx.fragment.app.FragmentActivity
3334
import androidx.lifecycle.lifecycleScope
3435
import androidx.lifecycle.ViewModelProvider
3536
import 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
3640
import github.aeonbtc.ibiswallet.ui.IbisWalletApp
3741
import github.aeonbtc.ibiswallet.ui.screens.CalculatorScreen
3842
import 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() {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package github.aeonbtc.ibiswallet.service
2+
3+
import android.app.Service
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.os.IBinder
7+
import androidx.core.content.ContextCompat
8+
import github.aeonbtc.ibiswallet.util.WalletNotificationHelper
9+
10+
class ConnectivityForegroundService : Service() {
11+
companion object {
12+
private const val ACTION_REFRESH = "github.aeonbtc.ibiswallet.action.REFRESH_CONNECTIVITY_SERVICE"
13+
14+
fun startOrUpdate(context: Context) {
15+
val intent =
16+
Intent(context, ConnectivityForegroundService::class.java).apply {
17+
action = ACTION_REFRESH
18+
}
19+
ContextCompat.startForegroundService(context, intent)
20+
}
21+
22+
fun stop(context: Context) {
23+
context.stopService(Intent(context, ConnectivityForegroundService::class.java))
24+
}
25+
}
26+
27+
override fun onBind(intent: Intent?): IBinder? = null
28+
29+
override fun onCreate() {
30+
super.onCreate()
31+
WalletNotificationHelper.ensureChannels(this)
32+
}
33+
34+
override fun onStartCommand(
35+
intent: Intent?,
36+
flags: Int,
37+
startId: Int,
38+
): Int {
39+
val snapshot = ConnectivityKeepAlivePolicy.currentSnapshot()
40+
if (intent?.action != ACTION_REFRESH || !snapshot.shouldRunForegroundService) {
41+
stopForegroundCompat()
42+
stopSelf()
43+
return START_NOT_STICKY
44+
}
45+
46+
val notification =
47+
WalletNotificationHelper.buildConnectivityForegroundNotification(
48+
context = this,
49+
snapshot = snapshot,
50+
)
51+
startForeground(WalletNotificationHelper.CONNECTIVITY_NOTIFICATION_ID, notification)
52+
return START_NOT_STICKY
53+
}
54+
55+
override fun onDestroy() {
56+
stopForegroundCompat()
57+
super.onDestroy()
58+
}
59+
60+
private fun stopForegroundCompat() {
61+
stopForeground(STOP_FOREGROUND_REMOVE)
62+
}
63+
}

0 commit comments

Comments
 (0)