secant-android-wallet/ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt

468 lines
16 KiB
Kotlin
Raw Normal View History

2021-10-09 07:36:58 -07:00
package cash.z.ecc.ui
2021-12-02 12:33:55 -08:00
import android.content.ClipData
import android.content.ClipboardManager
2021-12-06 12:31:39 -08:00
import android.content.Context
2022-02-17 05:08:06 -08:00
import android.content.Intent
import android.os.Bundle
2021-12-03 05:19:15 -08:00
import android.os.SystemClock
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
2021-12-03 05:19:15 -08:00
import androidx.annotation.VisibleForTesting
2021-12-09 12:18:18 -08:00
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
2021-12-09 12:18:18 -08:00
import androidx.compose.ui.Modifier
2021-12-03 05:19:15 -08:00
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
2021-12-06 12:31:39 -08:00
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
2021-12-20 05:18:06 -08:00
import cash.z.ecc.android.sdk.type.WalletBirthday
2021-12-09 12:21:30 -08:00
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.model.PersistableWallet
2021-12-09 12:21:30 -08:00
import cash.z.ecc.sdk.model.SeedPhrase
2022-02-17 05:08:06 -08:00
import cash.z.ecc.sdk.model.ZecRequest
import cash.z.ecc.sdk.send
2021-12-09 12:21:30 -08:00
import cash.z.ecc.sdk.type.fromResources
import cash.z.ecc.ui.design.compat.FontCompat
import cash.z.ecc.ui.design.component.GradientSurface
import cash.z.ecc.ui.screen.backup.view.BackupWallet
import cash.z.ecc.ui.screen.backup.viewmodel.BackupViewModel
import cash.z.ecc.ui.screen.home.model.spendableBalance
import cash.z.ecc.ui.screen.home.view.Home
import cash.z.ecc.ui.screen.home.viewmodel.SecretState
import cash.z.ecc.ui.screen.home.viewmodel.WalletViewModel
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
2022-01-13 09:49:08 -08:00
import cash.z.ecc.ui.screen.profile.view.Profile
2022-02-17 05:08:06 -08:00
import cash.z.ecc.ui.screen.request.view.Request
2021-12-09 12:21:30 -08:00
import cash.z.ecc.ui.screen.restore.view.RestoreWallet
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
import cash.z.ecc.ui.screen.seed.view.Seed
import cash.z.ecc.ui.screen.send.view.Send
import cash.z.ecc.ui.screen.settings.view.Settings
import cash.z.ecc.ui.screen.wallet_address.view.WalletAddresses
import cash.z.ecc.ui.theme.ZcashTheme
2021-12-06 12:31:39 -08:00
import kotlinx.coroutines.launch
2021-12-03 05:19:15 -08:00
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Suppress("TooManyFunctions")
class MainActivity : ComponentActivity() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val walletViewModel by viewModels<WalletViewModel>()
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
lateinit var navControllerForTesting: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2021-12-03 05:19:15 -08:00
setupSplashScreen()
if (FontCompat.isFontPrefetchNeeded()) {
2021-12-06 12:31:39 -08:00
lifecycleScope.launch {
FontCompat.prefetchFontsLegacy(applicationContext)
2021-12-03 05:19:15 -08:00
2021-12-06 12:31:39 -08:00
setupUiContent()
}
} else {
setupUiContent()
}
}
2021-12-03 05:19:15 -08:00
private fun setupSplashScreen() {
2021-12-09 12:18:18 -08:00
val splashScreen = installSplashScreen()
val start = SystemClock.elapsedRealtime().milliseconds
splashScreen.setKeepVisibleCondition {
if (SPLASH_SCREEN_DELAY > Duration.ZERO) {
val now = SystemClock.elapsedRealtime().milliseconds
2021-12-03 05:19:15 -08:00
2021-12-09 12:18:18 -08:00
// This delay is for debug purposes only; do not enable for production usage.
if (now - start < SPLASH_SCREEN_DELAY) {
return@setKeepVisibleCondition true
}
2021-12-03 05:19:15 -08:00
}
2021-12-09 12:18:18 -08:00
SecretState.Loading == walletViewModel.secretState.value
2021-12-03 05:19:15 -08:00
}
}
2021-12-06 12:31:39 -08:00
private fun setupUiContent() {
setContent {
ZcashTheme {
2021-12-09 12:18:18 -08:00
GradientSurface(
Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
when (val secretState = walletViewModel.secretState.collectAsState().value) {
SecretState.Loading -> {
2021-12-09 12:18:18 -08:00
// For now, keep displaying splash screen using condition above.
// In the future, we might consider displaying something different here.
}
SecretState.None -> {
2021-12-09 12:18:18 -08:00
WrapOnboarding()
}
is SecretState.NeedsBackup -> WrapBackup(secretState.persistableWallet)
is SecretState.Ready -> Navigation()
2021-12-06 12:31:39 -08:00
}
}
}
}
// Force collection to improve performance; sync can start happening while
// the user is going through the backup flow. Don't use eager collection in the view model,
// so that the collection is still tied to UI lifecycle.
lifecycleScope.launch {
walletViewModel.synchronizer.collect {
}
}
2021-12-06 12:31:39 -08:00
}
2021-12-09 12:21:30 -08:00
@Composable
private fun WrapOnboarding() {
val onboardingViewModel by viewModels<OnboardingViewModel>()
if (!onboardingViewModel.isImporting.collectAsState().value) {
Onboarding(
onboardingState = onboardingViewModel.onboardingState,
onImportWallet = { onboardingViewModel.isImporting.value = true },
onCreateWallet = {
walletViewModel.persistNewWallet()
}
)
reportFullyDrawn()
2021-12-09 12:21:30 -08:00
} else {
WrapRestore()
}
}
@Composable
private fun WrapBackup(persistableWallet: PersistableWallet) {
2021-12-09 12:21:30 -08:00
val backupViewModel by viewModels<BackupViewModel>()
2021-12-02 12:33:55 -08:00
BackupWallet(
persistableWallet, backupViewModel.backupState, backupViewModel.testChoices,
onCopyToClipboard = {
copyToClipboard(applicationContext, persistableWallet)
2021-12-02 12:33:55 -08:00
}, onComplete = {
2022-02-19 13:01:37 -08:00
walletViewModel.persistBackupComplete()
}
2021-12-02 12:33:55 -08:00
)
}
@Composable
2021-12-09 12:21:30 -08:00
private fun WrapRestore() {
val onboardingViewModel by viewModels<OnboardingViewModel>()
val restoreViewModel by viewModels<RestoreViewModel>()
when (val completeWordList = restoreViewModel.completeWordList.collectAsState().value) {
CompleteWordSetState.Loading -> {
// Although it might perform IO, it should be relatively fast.
// Consider whether to display indeterminate progress here.
// Another option would be to go straight to the restore screen with autocomplete
// disabled for a few milliseconds. Users would probably never notice due to the
// time it takes to re-orient on the new screen, unless users were doing this
// on a daily basis and become very proficient at our UI. The Therac-25 has
// historical precedent on how that could cause problems.
}
2021-12-09 12:21:30 -08:00
is CompleteWordSetState.Loaded -> {
RestoreWallet(
completeWordList.list,
restoreViewModel.userWordList,
onBack = { onboardingViewModel.isImporting.value = false },
paste = {
val clipboardManager = getSystemService(ClipboardManager::class.java)
return@RestoreWallet clipboardManager?.primaryClip?.toString()
},
onFinished = {
// Write the backup complete flag first, then the seed phrase. That avoids the UI
// flickering to the backup screen. Assume if a user is restoring from
// a backup, then the user has a valid backup.
walletViewModel.persistBackupComplete()
2021-12-20 05:18:06 -08:00
val network = ZcashNetwork.fromResources(application)
2021-12-09 12:21:30 -08:00
val restoredWallet = PersistableWallet(
2021-12-20 05:18:06 -08:00
network,
WalletBirthday(network.saplingActivationHeight),
2021-12-09 12:21:30 -08:00
SeedPhrase(restoreViewModel.userWordList.current.value)
)
walletViewModel.persistExistingWallet(restoredWallet)
}
)
}
}
}
2021-12-03 05:19:15 -08:00
@Composable
private fun Navigation() {
val navController = rememberNavController().also {
navControllerForTesting = it
}
NavHost(navController = navController, startDestination = NAV_HOME) {
composable(NAV_HOME) {
WrapHome(
goScan = {},
goProfile = { navController.navigate(NAV_PROFILE) },
goSend = { navController.navigate(NAV_SEND) },
2022-02-17 05:08:06 -08:00
goRequest = { navController.navigate(NAV_REQUEST) }
)
}
composable(NAV_PROFILE) {
2022-01-13 09:49:08 -08:00
WrapProfile(
onBack = { navController.popBackStack() },
onAddressDetails = { navController.navigate(NAV_WALLET_ADDRESS_DETAILS) },
2022-01-13 09:49:08 -08:00
onAddressBook = { },
onSettings = { navController.navigate(NAV_SETTINGS) },
onCoinholderVote = { },
onSupport = {}
)
2022-01-13 09:49:08 -08:00
}
composable(NAV_WALLET_ADDRESS_DETAILS) {
WrapWalletAddresses(
goBack = {
navController.popBackStack()
}
)
}
composable(NAV_SETTINGS) {
WrapSettings(
goBack = {
navController.popBackStack()
},
goWalletBackup = {
navController.navigate(NAV_SEED)
}
)
}
composable(NAV_SEED) {
WrapSeed(
goBack = {
navController.popBackStack()
}
)
}
2022-02-17 05:08:06 -08:00
composable(NAV_REQUEST) {
WrapRequest(goBack = { navController.popBackStack() })
}
composable(NAV_SEND) {
WrapSend(goBack = { navController.popBackStack() })
}
}
}
@Composable
private fun WrapHome(
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
Home(
walletSnapshot,
walletViewModel.transactionSnapshot.collectAsState().value,
goScan = goScan,
goRequest = goRequest,
goSend = goSend,
goProfile = goProfile
)
reportFullyDrawn()
}
2021-12-03 05:19:15 -08:00
}
2022-01-13 09:49:08 -08:00
@Composable
@Suppress("LongParameterList")
private fun WrapProfile(
onBack: () -> Unit,
onAddressDetails: () -> Unit,
onAddressBook: () -> Unit,
onSettings: () -> Unit,
onCoinholderVote: () -> Unit,
onSupport: () -> Unit
) {
val walletAddresses = walletViewModel.addresses.collectAsState().value
if (null == walletAddresses) {
// Display loading indicator
} else {
Profile(
walletAddresses.unified,
onBack = onBack,
onAddressDetails = onAddressDetails,
onAddressBook = onAddressBook,
onSettings = onSettings,
onCoinholderVote = onCoinholderVote,
onSupport = onSupport
)
}
}
@Composable
private fun WrapWalletAddresses(
goBack: () -> Unit,
) {
val walletAddresses = walletViewModel.addresses.collectAsState().value
if (null == walletAddresses) {
// Display loading indicator
} else {
WalletAddresses(
walletAddresses,
goBack
)
}
}
@Composable
private fun WrapSettings(
goBack: () -> Unit,
goWalletBackup: () -> Unit
) {
val synchronizer = walletViewModel.synchronizer.collectAsState().value
if (null == synchronizer) {
// Display loading indicator
} else {
Settings(
onBack = goBack,
onBackupWallet = goWalletBackup,
onRescanWallet = {
walletViewModel.rescanBlockchain()
}, onWipeWallet = {
2022-02-19 13:01:37 -08:00
walletViewModel.wipeWallet()
2022-02-19 13:01:37 -08:00
// If wipe ever becomes an operation to also delete the seed, then we'll also need
// to do the following to clear any retained state from onboarding (only happens if
// occurring during same session as onboarding)
// onboardingViewModel.onboardingState.goToBeginning()
// onboardingViewModel.isImporting.value = false
}
)
}
}
@Composable
private fun WrapSeed(
goBack: () -> Unit
) {
val persistableWallet = run {
val secretState = walletViewModel.secretState.collectAsState().value
if (secretState is SecretState.Ready) {
secretState.persistableWallet
} else {
null
}
}
val synchronizer = walletViewModel.synchronizer.collectAsState().value
if (null == synchronizer || null == persistableWallet) {
// Display loading indicator
} else {
Seed(
persistableWallet = persistableWallet,
onBack = goBack,
onCopyToClipboard = {
copyToClipboard(applicationContext, persistableWallet)
}
)
}
}
2022-02-17 05:08:06 -08:00
@Composable
private fun WrapRequest(
goBack: () -> Unit
) {
val walletAddresses = walletViewModel.addresses.collectAsState().value
if (null == walletAddresses) {
// Display loading indicator
} else {
Request(
walletAddresses.unified,
goBack = goBack,
onCreateAndSend = {
val chooserIntent = Intent.createChooser(it.newShareIntent(applicationContext), null)
startActivity(chooserIntent)
goBack()
},
)
}
}
@Composable
private fun WrapSend(
goBack: () -> Unit
) {
val synchronizer = walletViewModel.synchronizer.collectAsState().value
val spendableBalance = walletViewModel.walletSnapshot.collectAsState().value?.spendableBalance()
val spendingKey = walletViewModel.spendingKey.collectAsState().value
if (null == synchronizer || null == spendableBalance || null == spendingKey) {
// Display loading indicator
} else {
Send(
mySpendableBalance = spendableBalance,
goBack = goBack,
onCreateAndSend = {
synchronizer.send(spendingKey, it)
goBack()
},
)
}
}
2021-12-03 05:19:15 -08:00
companion object {
@VisibleForTesting
internal val SPLASH_SCREEN_DELAY = 0.seconds
@VisibleForTesting
const val NAV_HOME = "home"
@VisibleForTesting
const val NAV_PROFILE = "profile"
@VisibleForTesting
const val NAV_WALLET_ADDRESS_DETAILS = "wallet_address_details"
@VisibleForTesting
const val NAV_SETTINGS = "settings"
@VisibleForTesting
const val NAV_SEED = "seed"
2022-02-17 05:08:06 -08:00
@VisibleForTesting
const val NAV_REQUEST = "request"
@VisibleForTesting
const val NAV_SEND = "send"
2021-12-03 05:19:15 -08:00
}
}
2021-12-06 12:31:39 -08:00
private fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText(
context.getString(R.string.new_wallet_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
clipboardManager.setPrimaryClip(data)
}
2022-02-17 05:08:06 -08:00
private fun ZecRequest.newShareIntent(context: Context) = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, context.getString(R.string.request_template_format, toUri()))
type = "text/plain"
}