package com.tangem.tap import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.MotionEvent import android.view.WindowManager import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import com.tangem.common.routing.AppRouter import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter 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.navigation.url.UrlOpener import com.tangem.core.ui.extensions.LocalUserInteractionTracker import com.tangem.core.ui.extensions.UserInteractionTracker import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.analytics.events.Push import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.extensions.uriValidate import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.seconds lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService internal var lockUserWalletsTimer: LockUserWalletsTimer? = null private set private val coroutineContext: CoroutineContext get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope") val scope = CoroutineScope(coroutineContext) private val mainCoroutineContext: CoroutineContext get() = SupervisorJob() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) @Suppress("LargeClass") @AndroidEntryPoint class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { /** Router for opening tester menu */ @Inject lateinit var cardSdkOwner: CardSdkOwner @Inject lateinit var cardSdkConfigRepository: CardSdkConfigRepository @Inject lateinit var injectedTangemSdkManager: TangemSdkManager @Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext @Inject internal lateinit var appRouterConfig: AppRouterConfig @Inject internal lateinit var appRouter: AppRouter @Inject internal lateinit var routingComponentFactory: RoutingComponent.Factory @Inject lateinit var cardRepository: CardRepository @Inject lateinit var backupServiceHolder: BackupServiceHolder @Inject lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase @Inject lateinit var setGooglePayAvailabilityUseCase: SetGooglePayAvailabilityUseCase @Inject lateinit var dispatchers: CoroutineDispatcherProvider @Inject internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector @Inject internal lateinit var deeplinkFactory: DeepLinkFactory @Inject internal lateinit var urlOpener: UrlOpener @Inject internal lateinit var testerMenuLauncher: TesterMenuLauncher @Inject internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler @Inject internal lateinit var userWalletsListRepository: UserWalletsListRepository @Inject internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase @Inject internal lateinit var passwordRequester: HotWalletPasswordRequester private val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { TangemLogger.i("onCreate: data=${intent?.data}, extras=${intent?.extras?.keySet()}") // We need to call it before onCreate to prevent unnecessary activity recreation installAppTheme() val splashScreen = installSplashScreen() TangemLogger.i("Splash screen installed") enableEdgeToEdge( navigationBarStyle = SystemBarStyle.auto( Color.Transparent.toArgb(), Color.Transparent.toArgb(), ), ) super.onCreate(savedInstanceState) // We have to allow adjust_resize (can only be specified in the manifest) for Android <=10, // in order for imePadding to work correctly // for Android 11+ we set SOFT_INPUT_ADJUST_NOTHING to prevent resizing the layout // so that we don't have any distortions in the layout when displaying the keyboard // https://issuetracker.google.com/issues/266331465 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { window.setHideOverlayWindows(true) } splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } installActivityDependencies() observeAppThemeModeUpdates() setRootContent() initContent() sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() lifecycle.addObserver(WindowObscurationObserver) lifecycle.addObserver(defaultDeviceFlipDetector) if (BuildConfig.TESTER_MENU_ENABLED) { lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver) } if (intent != null) { handleDeepLink(intent = intent, isFromOnNewIntent = false) } } private fun setRootContent() { // for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy // doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it // we try to init component with previous stack val routingComponent = routingComponentFactory.create( context = rootComponentContext, initialStack = appRouterConfig.stack, launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intent), ) val userInteractionTracker = object : UserInteractionTracker { override fun onUserInteraction() { lockUserWalletsTimer?.restart() } } setContent { CompositionLocalProvider(LocalUserInteractionTracker provides userInteractionTracker) { routingComponent.Content( Modifier .fillMaxSize() .semantics { testTagsAsResourceId = true }, ) } } } private fun installActivityDependencies() { cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this) backupService = requireNotNull(backupServiceHolder.backupService.get()) // will be deleted eventually lockUserWalletsTimer = LockUserWalletsTimer( context = this, settingsRepository = settingsRepository, coroutineScope = mainScope, userWalletsListRepository = userWalletsListRepository, clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase, passwordRequester = passwordRequester, appRouter = appRouter, ) } private fun installAppTheme() { appThemeModeFlow = createAppThemeModeFlow() val mode = runBlocking { withTimeoutOrNull(APP_THEME_LOAD_TIMEOUT.seconds) { appThemeModeFlow.first() } ?: AppThemeMode.DEFAULT } updateAppTheme(mode) } private fun observeAppThemeModeUpdates() { appThemeModeFlow .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 = requireNotNull(application as? TangemApplication) { "Application is null" } return tangemApplication.getAppThemeModeUseCase() .filterNotNull() .distinctUntilChanged() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } .shareIn( scope = lifecycleScope, started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), ) } override fun onStart() { super.onStart() TangemLogger.i("onStart") } override fun onStop() { super.onStop() TangemLogger.i("onStop") } override fun onDestroy() { TangemLogger.i("onDestroy") // workaround: kill process when activity destroy to avoid state when lock() wallets // and navigation to unlock screen was skipped because system kills activity but not process if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) { android.os.Process.killProcess(android.os.Process.myPid()) } super.onDestroy() } private fun updateAppTheme(appThemeMode: AppThemeMode) { 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) MutableAppThemeModeHolder.value = appThemeMode MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() } 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) TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}") val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true if (isFromPush) { analyticsEventsHandler.send(Push.PushNotificationOpened()) } handleDeepLink(intent = intent, isFromOnNewIntent = true) } 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() } override fun dispatchTouchEvent(event: MotionEvent): Boolean { val isHandled = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) return if (isHandled) super.dispatchTouchEvent(event) else false } override fun dispatchKeyEvent(event: KeyEvent): Boolean { return if (BuildConfig.TESTER_MENU_ENABLED) { testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) } else { super.dispatchKeyEvent(event) } } private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) { val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri() val webLink = intent.getStringExtra(WEBLINK_KEY) val receivedDeepLink = intent.data ?: deepLinkExtras when { receivedDeepLink != null -> { deeplinkFactory.handleDeeplink( deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope, isFromOnNewIntent = isFromOnNewIntent, ) } webLink?.uriValidate() == true -> { urlOpener.openUrl(webLink) } } } private fun sendStakingUnsubmittedHashes() { lifecycleScope.launch { sendUnsubmittedHashesUseCase.invoke() .onLeft { TangemLogger.e(it.toString()) } .onRight { TangemLogger.d("Submitting hashes succeeded") } } } private fun checkGoogleServicesAvailability() { val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this) lifecycleScope.launch { setGoogleServicesAvailabilityUseCase(isGoogleServicesAvailable) if (isGoogleServicesAvailable) { val paymentsClient = GoogleServicesHelper.createPaymentsClient(this@MainActivity) val isGooglePayAvailable = GoogleServicesHelper.checkGooglePayAvailability(paymentsClient) setGooglePayAvailabilityUseCase(isGooglePayAvailable) } else { setGooglePayAvailabilityUseCase(false) } } } companion object { private const val APP_THEME_LOAD_TIMEOUT = 2 private const val MOCKED_BUILD_TYPE = "mocked" private const val OPENED_FROM_GCM_PUSH = "google.sent_time" // every bundle from FCM contains this key } }