package com.bitcointxoko.gudariwallet 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 import android.os.Bundle import android.os.PowerManager import android.provider.Settings import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.* import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore import com.bitcointxoko.gudariwallet.service.WalletNotificationService import com.bitcointxoko.gudariwallet.ui.onboarding.OnboardingScreen import com.bitcointxoko.gudariwallet.ui.WalletScreen import com.bitcointxoko.gudariwallet.ui.WalletViewModel import com.bitcointxoko.gudariwallet.ui.WalletViewModelFactory import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel import com.bitcointxoko.gudariwallet.ui.theme.GudariWalletTheme import com.bitcointxoko.gudariwallet.util.NotificationHelper import com.bitcointxoko.gudariwallet.util.IntentUtils import kotlinx.coroutines.flow.MutableSharedFlow import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import cafe.adriel.lyricist.ProvideStrings import com.bitcointxoko.gudariwallet.data.HistoricalSyncStore import com.bitcointxoko.gudariwallet.util.NotificationConstants class MainActivity : AppCompatActivity() { private lateinit var secretStore: EncryptedSecretStore private val vm: WalletViewModel by viewModels { WalletViewModelFactory(application) } // ── NFC ────────────────────────────────────────────────────────────────── val nfcVm: NfcViewModel by viewModels() private var nfcAdapter: NfcAdapter? = null /** Pending intent used by foreground dispatch — routes tag intents back here. */ private val nfcPendingIntent: PendingIntent by lazy { PendingIntent.getActivity( this, 0, Intent(this, javaClass).apply { addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) }, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } // ── Existing state ─────────────────────────────────────────────────────── // Holds a paymentHash that arrived via notification tap. // WalletScreen observes this and navigates to the detail screen. // MutableState so Compose recomposes when it changes. private val pendingPaymentHash = mutableStateOf(null) val pendingNavigateToHistory = mutableStateOf(false) // Holds a payment URI that arrived via deep link / intent (BTCPay, external apps). private val pendingPaymentUri = mutableStateOf(null) private val requestNotificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { /* informational */ } // Broadcasts window focus gains to any observer in the composition val windowFocusEvents = MutableSharedFlow(extraBufferCapacity = 1) override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) windowFocusEvents.tryEmit(Unit) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) secretStore = EncryptedSecretStore(applicationContext) val syncStore = HistoricalSyncStore(applicationContext) val credentialsState = secretStore.credentials .stateIn( scope = lifecycleScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null ) // Must be first — before any notification is posted // NotificationHelper.createChannels(applicationContext) // Read hash from the intent that launched the activity (cold start from notification) pendingPaymentHash.value = intent.getStringExtra(NotificationHelper.EXTRA_PAYMENT_HASH) if (intent.getBooleanExtra(NotificationConstants.NAVIGATE_TO_HISTORY, false)) { pendingNavigateToHistory.value = true } // Read payment URI from the intent that launched the activity (cold start from deep link) pendingPaymentUri.value = IntentUtils.extractPaymentUri(intent) // Initialise NFC adapter (null on devices without NFC — handled gracefully) nfcAdapter = NfcAdapter.getDefaultAdapter(this) nfcVm.onNfcIntent(intent) enableEdgeToEdge() setContent { GudariWalletTheme { // ── Lyricist ────────────────────────────────────────────────── val lyricist = rememberAppStrings() ProvideStrings(lyricist, LocalAppStrings) { // ───────────────────────────────────────────────────────────── val credentials by credentialsState.collectAsState() val isOnboarded = credentials?.isOnboarded == true if (isOnboarded) { LaunchedEffect(Unit) { WalletNotificationService.start(this@MainActivity) requestNotificationPermissionIfNeeded() } WalletScreen( vm = vm, nfcVm = nfcVm, pendingPaymentHash = pendingPaymentHash, pendingNavigateToHistory = pendingNavigateToHistory, pendingPaymentUri = pendingPaymentUri, lyricist = lyricist ) } else { OnboardingScreen( secretStore = secretStore, syncStore = syncStore, onRequestNotificationPermission = { requestNotificationPermissionIfNeeded() }, onComplete = {} ) } } } } } // ── NFC foreground dispatch ─────────────────────────────────────────────── override fun onResume() { super.onResume() // 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, intentFilters, techLists ) } override fun onPause() { super.onPause() nfcAdapter?.disableForegroundDispatch(this) } // ── Intent routing ──────────────────────────────────────────────────────── // Called when the activity is already running and a notification is tapped, // a deep link arrives, or an NFC tag is discovered while foregrounded. // (FLAG_ACTIVITY_SINGLE_TOP prevents a new instance being created) override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) // Notification tap → navigate to payment detail val hash = intent.getStringExtra(NotificationHelper.EXTRA_PAYMENT_HASH) if (hash != null) { pendingPaymentHash.value = hash } if (intent.getBooleanExtra(NotificationConstants.NAVIGATE_TO_HISTORY, false)) { pendingNavigateToHistory.value = true } // Deep link / external app URI val uri = IntentUtils.extractPaymentUri(intent) if (uri != null) { pendingPaymentUri.value = uri } // NFC tag discovered → hand off to NfcViewModel for reading 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() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) } } }