package com.tangem.tap import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.res.Configuration import android.nfc.NfcAdapter import android.os.Bundle import android.view.View import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding import com.arkivanov.decompose.value.observe import com.arkivanov.essenty.lifecycle.asEssentyLifecycle import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.data.card.sdk.CardSdkLifecycleObserver import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.operations.backup.BackupService import com.tangem.sdk.extensions.init import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.routing.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.Inject import kotlin.coroutines.CoroutineContext lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService internal var lockUserWalletsTimer: LockUserWalletsTimer? = null private set var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext get() = Job() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope") val scope = CoroutineScope(coroutineContext) private val mainCoroutineContext: CoroutineContext get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) @Suppress("LargeClass") @AndroidEntryPoint class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { @Inject lateinit var appStateHolder: AppStateHolder /** Router for opening tester menu */ @Inject lateinit var testerRouter: TesterRouter @Inject lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver @Inject lateinit var cardSdkConfigRepository: CardSdkConfigRepository @Inject lateinit var injectedTangemSdkManager: TangemSdkManager @Inject lateinit var scanCardUseCase: ScanCardUseCase @Inject lateinit var walletRouter: WalletRouter @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter @Inject lateinit var manageTokensUi: ManageTokensUi @Inject lateinit var walletConnectInteractor: WalletConnectInteractor @Inject lateinit var sendRouter: SendRouter @Inject lateinit var qrScanningRouter: QrScanningRouter @Inject lateinit var deepLinksRegistry: DeepLinksRegistry @Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase @Inject lateinit var getPolkadotCheckHasImmortalUseCase: GetPolkadotCheckHasImmortalUseCase @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @Inject lateinit var userWalletsListManager: UserWalletsListManager @Inject lateinit var emailSender: EmailSender @Inject lateinit var stakingRouter: StakingRouter @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext @Inject internal lateinit var appRouterConfig: AppRouterConfig @Inject internal lateinit var routingComponentFactory: RoutingComponent.Factory @Inject lateinit var disclaimerRouter: DisclaimerRouter @Inject lateinit var pushNotificationsRouter: PushNotificationsRouter @Inject lateinit var cardRepository: CardRepository internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() private var snackbar: Snackbar? = null private val dialogManager = DialogManager() private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation enableEdgeToEdge( navigationBarStyle = SystemBarStyle.auto( Color.Transparent.toArgb(), Color.Transparent.toArgb(), ), ) super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } installActivityDependencies() observeAppThemeModeUpdates() setContentView(R.layout.activity_main) installRouting() initContent() observeStateUpdates() observePolkadotAccountHealthCheck() if (intent != null) { deepLinksRegistry.launch(intent) } } private fun installRouting() { val routingComponent = routingComponentFactory.create( context = rootComponentContext, ) appRouterConfig.routerScope = lifecycleScope appRouterConfig.componentRouter = routingComponent.router routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> appRouterConfig.stack = childStack.backStack .plus(childStack.active) .map { it.configuration } when (val child = childStack.active.instance) { is RoutingComponent.Child.Initial -> Unit is RoutingComponent.Child.LegacyFragment -> { supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider) } is RoutingComponent.Child.LegacyIntent -> { startActivity(child.intent) } } } } private fun observeStateUpdates() { viewModel.state .flowWithLifecycle(lifecycle) .onEach { state -> if (state.toast is StateEvent.Triggered) { showToast(state.toast.data) state.toast.onConsume() } } .launchIn(lifecycleScope) } private fun showToast(toast: Toast) { dismissSnackbar() showSnackbar(toast.message, Snackbar.LENGTH_LONG, toast.action.text) { toast.action.onClick() dismissSnackbar() } } private fun installActivityDependencies() { cardSdkLifecycleObserver.onCreate(context = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer( owner = this, settingsRepository = settingsRepository, userWalletsListManager = userWalletsListManager, ) initIntentHandlers() store.dispatch( DaggerGraphAction.SetActivityDependencies( testerRouter = testerRouter, scanCardUseCase = scanCardUseCase, walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, manageTokensUi = manageTokensUi, cardSdkConfigRepository = cardSdkConfigRepository, sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, emailSender = emailSender, stakingRouter = stakingRouter, disclaimerRouter = disclaimerRouter, pushNotificationsRouter = pushNotificationsRouter, ), ) } private fun installAppTheme() { appThemeModeFlow = createAppThemeModeFlow() val mode = runBlocking { appThemeModeFlow.filterNotNull().first() } updateAppTheme(mode) } private fun observeAppThemeModeUpdates() { appThemeModeFlow .filterNotNull() .flowWithLifecycle(lifecycle) .onEach(::updateAppTheme) .launchIn(lifecycleScope) } @SuppressLint("SourceLockedOrientationActivity") private fun initContent() { supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), true, ) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } private fun createAppThemeModeFlow(): SharedFlow { val tangemApplication = application as TangemApplication return tangemApplication.getAppThemeModeUseCase() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } .shareIn( scope = lifecycleScope + Dispatchers.IO, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), ) } override fun onStart() { super.onStart() dialogManager.onStart(this) } override fun onResume() { super.onResume() val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) if (nfcAdapter?.isEnabled == true) { val pendingIntent = PendingIntent.getActivity( this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, ) val intentFilters = arrayOf( IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), ) nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) } // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) navigateToInitialScreenIfNeeded(intent) } override fun onPause() { super.onPause() val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) if (nfcAdapter?.isEnabled == true) { nfcAdapter.disableForegroundDispatch(this) } } override fun onStop() { notificationsHandler = null dialogManager.onStop() super.onStop() } override fun onDestroy() { intentProcessor.removeAll() cardSdkLifecycleObserver.onDestroy(this) super.onDestroy() } private fun initIntentHandlers() { val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) } private fun updateAppTheme(appThemeMode: AppThemeMode) { MutableAppThemeModeHolder.value = appThemeMode MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() val mode = when (appThemeMode) { AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } setDefaultNightMode(mode) delegate.localNightMode = mode } private fun isDarkTheme(): Boolean { return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true Configuration.UI_MODE_NIGHT_NO -> false Configuration.UI_MODE_NIGHT_UNDEFINED -> false else -> false } } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) /* * FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called * inside IntentHandler. */ cardSdkLifecycleObserver.onCreate(context = this) lifecycleScope.launch { intentProcessor.handleIntent(intent, true) } if (intent != null) { deepLinksRegistry.launch(intent) } } override fun showSnackbar( @StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: View.OnClickListener?, ) { showSnackbar(getString(text), length, buttonTitle?.let(::getString), action) } override fun showSnackbar( text: TextReference, length: Int, buttonTitle: TextReference?, action: View.OnClickListener?, ) { showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action) } override fun dismissSnackbar() { snackbar?.dismiss() snackbar = null } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) } } override fun addOnActivityResultCallback(callback: OnActivityResultCallback) { onActivityResultCallbacks.remove(callback) onActivityResultCallbacks.add(callback) } override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) { onActivityResultCallbacks.remove(callback) } override fun onUserInteraction() { super.onUserInteraction() lockUserWalletsTimer?.restart() } private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) { if (snackbar != null) return snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply { val textColor = getColor(R.color.text_primary_2) setBackgroundTint(getColor(R.color.button_primary)) setActionTextColor(textColor) setTextColor(textColor) if (buttonTitle != null && action != null) { setAction(buttonTitle, action) } addCallback( object : BaseTransientBottomBar.BaseCallback() { override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { snackbar = null removeCallback(this) } }, ) } snackbar?.show() } private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted when { !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { navigateToInitialScreen(intentWhichStartedActivity) } backStackIsEmpty -> { navigateToInitialScreen(intentWhichStartedActivity) } else -> Unit } } private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) } } else { lifecycleScope.launch { val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) val isEnabled = toggles.isPushNotificationsEnabled && !cardRepository.isTangemTOSAccepted() val route = if (isEnabled) AppRoute.Disclaimer(isTosAccepted = false) else AppRoute.Home store.dispatchNavigationAction { replaceAll(route) } intentProcessor.handleIntent(intentWhichStartedActivity, false) } } store.dispatch(BackupAction.CheckForUnfinishedBackup) } private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() .flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED) .distinctUntilChanged() .collect { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it.second)) } } lifecycleScope.launch { getPolkadotCheckHasImmortalUseCase() .flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED) .distinctUntilChanged() .collect { analyticsEventsHandler.send( WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it.second), ) } } } }