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.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 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) // Do NOT add FLAG_ACTIVITY_NEW_TASK — this causes // balDontBringExistingBackgroundTaskStackToFg = true }, 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) // 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 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) // 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 { val credentials by credentialsState.collectAsState() // Treat null (loading) the same as not-onboarded to avoid a flash val isOnboarded = credentials?.isOnboarded == true if (isOnboarded) { LaunchedEffect(Unit) { WalletNotificationService.start(this@MainActivity) requestNotificationPermissionIfNeeded() promptBatteryOptimizationIfNeeded() } WalletScreen( vm = vm, nfcVm = nfcVm, pendingPaymentHash = pendingPaymentHash, pendingPaymentUri = pendingPaymentUri ) } else { OnboardingScreen( secretStore = secretStore, onComplete = { /* no-op: Flow update triggers recomposition automatically */ } ) } } } } // ── 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 } // 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) } } private fun promptBatteryOptimizationIfNeeded() { val pm = getSystemService(POWER_SERVICE) as PowerManager if (pm.isIgnoringBatteryOptimizations(packageName)) return val prefs = getSharedPreferences("gudari_ui_prefs", MODE_PRIVATE) if (prefs.getBoolean("battery_opt_prompted", false)) return prefs.edit { putBoolean("battery_opt_prompted", true) } AlertDialog.Builder(this) .setTitle("Enable background notifications") .setMessage( "To receive payment alerts when the app is closed, allow Gudari Wallet " + "to run without battery restrictions.\n\n" + "Tap 'Open Settings', then select Battery → Unrestricted." ) .setPositiveButton("Open Settings") { _, _ -> openBatterySettings() } .setNegativeButton("Not now", null) .show() } private fun openBatterySettings() { val appDetailsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = "package:$packageName".toUri() } if (tryStartActivity(appDetailsIntent)) return tryStartActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) } private fun tryStartActivity(intent: Intent): Boolean { return try { startActivity(intent); true } catch (e: ActivityNotFoundException) { false } } }