288 lines
11 KiB
Kotlin
288 lines
11 KiB
Kotlin
@file:Suppress("DEPRECATION")
|
|
|
|
package co.electriccoin.zcash.ui
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Intent
|
|
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.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
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 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.viewmodel.AuthenticationUIState
|
|
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
|
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
|
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
|
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
|
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.OnboardingNavigation
|
|
import co.electriccoin.zcash.ui.screen.scan.thirdparty.ThirdPartyScan
|
|
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.android.ext.android.inject
|
|
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 oldHomeViewModel by viewModel<OldHomeViewModel>()
|
|
|
|
val walletViewModel by viewModel<WalletViewModel>()
|
|
|
|
val storageCheckViewModel by viewModel<StorageCheckViewModel>()
|
|
|
|
internal val authenticationViewModel by viewModel<AuthenticationViewModel>()
|
|
|
|
lateinit var navControllerForTesting: NavHostController
|
|
|
|
val configurationOverrideFlow = MutableStateFlow<ConfigurationOverride?>(null)
|
|
|
|
private val navigationRouter: NavigationRouter by inject()
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
Twig.debug { "Activity state: Create" }
|
|
|
|
setAllowedScreenOrientation()
|
|
|
|
setupSplashScreen()
|
|
|
|
setupUiContent()
|
|
|
|
monitorForBackgroundSync()
|
|
|
|
if (intent.data != null) {
|
|
navigationRouter.forward(ThirdPartyScan)
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
|
|
if (intent.data != null) {
|
|
navigationRouter.forward(ThirdPartyScan)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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) {
|
|
val isHideBalances by oldHomeViewModel.isHideBalances.collectAsStateWithLifecycle()
|
|
ZcashTheme(
|
|
balancesAvailable = isHideBalances == false
|
|
) {
|
|
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()
|
|
},
|
|
onFail = {
|
|
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 secretState by walletViewModel.secretState.collectAsStateWithLifecycle()
|
|
|
|
when (secretState) {
|
|
SecretState.NONE -> {
|
|
OnboardingNavigation()
|
|
}
|
|
|
|
SecretState.READY -> {
|
|
Navigation()
|
|
}
|
|
|
|
SecretState.LOADING -> {
|
|
// For now, keep displaying splash screen using condition above.
|
|
// In the future, we might consider displaying something different here.
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun monitorForBackgroundSync() {
|
|
val isEnableBackgroundSyncFlow =
|
|
run {
|
|
val isSecretReadyFlow = walletViewModel.secretState.map { it == SecretState.READY }
|
|
val isBackgroundSyncEnabledFlow = oldHomeViewModel.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
|
|
}
|
|
}
|