@file:Suppress("DEPRECATION") package co.electriccoin.zcash.ui import android.annotation.SuppressLint import android.content.pm.ActivityInfo import android.os.Bundle import android.os.SystemClock import androidx.activity.enableEdgeToEdge import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavHostController import cash.z.ecc.android.sdk.fixture.WalletFixture import cash.z.ecc.android.sdk.model.SeedPhrase import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.sdk.type.fromResources import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider import co.electriccoin.zcash.ui.common.extension.setContentCompat import co.electriccoin.zcash.ui.common.model.OnboardingState import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationUIState import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.Override import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase import co.electriccoin.zcash.ui.screen.authentication.RETRY_TRIGGER_DELAY import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs import co.electriccoin.zcash.ui.screen.seed.WrapSeed import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.work.WorkIds import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class MainActivity : FragmentActivity() { private val homeViewModel by viewModel() val walletViewModel by viewModel() val storageCheckViewModel by viewModel() internal val authenticationViewModel by viewModel() lateinit var navControllerForTesting: NavHostController val configurationOverrideFlow = MutableStateFlow(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Twig.debug { "Activity state: Create" } setAllowedScreenOrientation() setupSplashScreen() setupUiContent() monitorForBackgroundSync() } override fun onStart() { Twig.debug { "Activity state: Start" } authenticationViewModel.runAuthenticationRequiredCheck() super.onStart() } override fun onStop() { Twig.debug { "Activity state: Stop" } authenticationViewModel.persistGoToBackgroundTime(System.currentTimeMillis()) super.onStop() } /** * Sets whether the screen rotation is enabled or screen orientation is locked in the portrait mode. */ @SuppressLint("SourceLockedOrientationActivity") private fun setAllowedScreenOrientation() { requestedOrientation = if (BuildConfig.IS_SCREEN_ROTATION_ENABLED) { ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } else { ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } } private fun setupSplashScreen() { val splashScreen = installSplashScreen() val start = SystemClock.elapsedRealtime().milliseconds splashScreen.setKeepOnScreenCondition { if (SPLASH_SCREEN_DELAY > Duration.ZERO) { val now = SystemClock.elapsedRealtime().milliseconds // This delay is for debug purposes only; do not enable for production usage. if (now - start < SPLASH_SCREEN_DELAY) { return@setKeepOnScreenCondition true } } // Note this condition needs to be kept in sync with the condition in MainContent() homeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value } } private fun setupUiContent() { // Turn off the decor fitting system windows, which allows us to handle insets, // including IME animations, and go edge-to-edge. // This also sets up the initial system bar style based on the platform theme enableEdgeToEdge() setContentCompat { Override(configurationOverrideFlow) { ZcashTheme { BlankSurface( Modifier .fillMaxWidth() .fillMaxHeight() .imePadding() ) { BindCompLocalProvider { MainContent() AuthenticationForAppAccess() } } } } // Force collection to improve performance; sync can start happening while // the user is going through the backup flow. walletViewModel.synchronizer.collectAsStateWithLifecycle() } } @Composable private fun AuthenticationForAppAccess() { val authState = authenticationViewModel.appAccessAuthenticationResultState.collectAsStateWithLifecycle().value val animateAppAccess = authenticationViewModel.showWelcomeAnimation.collectAsStateWithLifecycle().value val authFailed = authenticationViewModel.authFailed.collectAsStateWithLifecycle().value if (animateAppAccess) { WelcomeAnimationAutostart( delay = AnimationConstants.INITIAL_DELAY.milliseconds, showAuthLogo = authFailed, onRetry = { authenticationViewModel.resetAuthenticationResult() authenticationViewModel.authenticate( activity = this, initialAuthSystemWindowDelay = RETRY_TRIGGER_DELAY.milliseconds, useCase = AuthenticationUseCase.AppAccess ) } ) } when (authState) { AuthenticationUIState.Initial -> { Twig.debug { "Authentication initial state" } // Wait for the state update } AuthenticationUIState.NotRequired -> { Twig.debug { "App access authentication NOT required - welcome animation only" } // Wait until the welcome animation finishes then mark it was shown LaunchedEffect(key1 = authenticationViewModel.showWelcomeAnimation) { delay(AnimationConstants.together()) authenticationViewModel.setWelcomeAnimationDisplayed() } } AuthenticationUIState.Required -> { Twig.debug { "App access authentication required" } // Check and trigger app access authentication if required // Note that the Welcome animation is part of its logic WrapAuthentication( onSuccess = { lifecycleScope.launch { // Wait until the welcome animation finishes, then mark it as presented to the user delay((AnimationConstants.durationOnly()).milliseconds) authenticationViewModel.appAccessAuthentication.value = AuthenticationUIState.Successful } }, onCancel = { authenticationViewModel.setAuthFailed() }, onFailed = { authenticationViewModel.setAuthFailed() }, useCase = AuthenticationUseCase.AppAccess ) } AuthenticationUIState.Successful -> { Twig.debug { "Authentication successful - entering the app" } // No action is needed - the main app content is laid out now } } } @Composable private fun MainContent() { val configuration = homeViewModel.configurationFlow.collectAsStateWithLifecycle().value val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value // Note this condition needs to be kept in sync with the condition in setupSplashScreen() if (null == configuration || secretState == SecretState.Loading) { // For now, keep displaying splash screen using condition above. // In the future, we might consider displaying something different here. } else { // Note that the deeply nested child views will probably receive arguments derived from // the configuration. The CompositionLocalProvider is helpful for passing the configuration // to the "platform" layer, which is where the arguments will be derived from. CompositionLocalProvider(RemoteConfig provides configuration) { when (secretState) { SecretState.None -> { WrapOnboarding() } is SecretState.NeedsWarning -> { WrapSecurityWarning( onBack = { walletViewModel.persistOnboardingState(OnboardingState.NONE) }, onConfirm = { walletViewModel.persistOnboardingState(OnboardingState.NEEDS_BACKUP) if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) { persistExistingWalletWithSeedPhrase( applicationContext, walletViewModel, SeedPhrase.new(WalletFixture.Alice.seedPhrase), WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext)) ) } else { walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING) } } ) } is SecretState.NeedsBackup -> { WrapSeed( args = SeedNavigationArgs.NEW_WALLET, goBackOverride = null ) } is SecretState.Ready -> { Navigation() } else -> { error("Unhandled secret state: $secretState") } } } } } private fun monitorForBackgroundSync() { val isEnableBackgroundSyncFlow = run { val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready } val isBackgroundSyncEnabledFlow = homeViewModel.isBackgroundSyncEnabled.filterNotNull() isSecretReadyFlow.combine(isBackgroundSyncEnabledFlow) { isSecretReady, isBackgroundSyncEnabled -> isSecretReady && isBackgroundSyncEnabled } } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { isEnableBackgroundSyncFlow.collect { isEnableBackgroundSync -> if (isEnableBackgroundSync) { WorkIds.enableBackgroundSynchronization(application) } else { WorkIds.disableBackgroundSynchronization(application) } } } } } companion object { @VisibleForTesting internal val SPLASH_SCREEN_DELAY = 0.seconds } }