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.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 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.ui.common.compose.BindCompLocalProvider import co.electriccoin.zcash.ui.common.model.OnboardingState 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.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.Override import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.screen.newwalletrecovery.WrapNewWalletRecovery 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.warning.WrapNotEnoughSpace import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.work.WorkIds 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 kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class MainActivity : ComponentActivity() { val homeViewModel by viewModels() val walletViewModel by viewModels() @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val storageCheckViewModel by viewModels() lateinit var navControllerForTesting: NavHostController val configurationOverrideFlow = MutableStateFlow(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setAllowedScreenOrientation() setupSplashScreen() setupUiContent() monitorForBackgroundSync() } /** * 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() setContent { Override(configurationOverrideFlow) { ZcashTheme { GradientSurface( Modifier .fillMaxWidth() .fillMaxHeight() ) { BindCompLocalProvider { val isEnoughSpace by storageCheckViewModel.isEnoughSpace.collectAsStateWithLifecycle() if (isEnoughSpace == false) { WrapNotEnoughSpace() } else { MainContent() } } } } } // Force collection to improve performance; sync can start happening while // the user is going through the backup flow. walletViewModel.synchronizer.collectAsStateWithLifecycle() } } @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.persistNewWallet() } } ) } is SecretState.NeedsBackup -> { WrapNewWalletRecovery( secretState.persistableWallet, onBackupComplete = { walletViewModel.persistOnboardingState(OnboardingState.READY) } ) } is SecretState.Ready -> { Navigation() } else -> { error("Unhandled secret state: $secretState") } } } } } private fun monitorForBackgroundSync() { val isEnableBackgroundSyncFlow = run { val homeViewModel by viewModels() 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 } }