2019-11-26 12:29:16 -08:00
package cash.z.ecc.android.ui.home
2019-12-17 13:32:57 -08:00
import android.content.Context
2019-11-26 12:29:16 -08:00
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
2019-11-27 06:24:00 -08:00
import android.widget.TextView
2019-12-17 13:32:57 -08:00
import androidx.lifecycle.lifecycleScope
2019-11-26 12:29:16 -08:00
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
2020-01-05 21:01:06 -08:00
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
2020-01-15 08:27:09 -08:00
import cash.z.ecc.android.ext.disabledIf
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.toColoredSpan
2019-11-26 12:29:16 -08:00
import cash.z.ecc.android.ui.base.BaseFragment
2019-11-27 06:24:00 -08:00
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
2020-01-05 21:01:06 -08:00
import cash.z.ecc.android.ui.send.SendViewModel
2019-12-17 13:32:57 -08:00
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
2019-12-23 11:19:47 -08:00
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
2020-01-13 16:09:22 -08:00
import cash.z.wallet.sdk.Synchronizer
2020-01-15 08:27:09 -08:00
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
2019-12-23 11:19:47 -08:00
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.convertZecToZatoshi
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import cash.z.wallet.sdk.ext.twig
2019-11-27 06:24:00 -08:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2020-01-09 23:53:16 -08:00
import kotlinx.coroutines.delay
2019-12-23 11:19:47 -08:00
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
2019-11-26 12:29:16 -08:00
class HomeFragment : BaseFragment < FragmentHomeBinding > ( ) {
2019-12-13 02:59:56 -08:00
private lateinit var numberPad : List < TextView >
2019-12-23 11:19:47 -08:00
private lateinit var uiModel : HomeViewModel . UiModel
2019-12-13 02:59:56 -08:00
2020-01-06 22:45:24 -08:00
private val walletSetup : WalletSetupViewModel by activityViewModel ( false )
2020-01-05 21:01:06 -08:00
private val sendViewModel : SendViewModel by activityViewModel ( )
private val viewModel : HomeViewModel by viewModel ( )
2019-12-23 11:19:47 -08:00
2020-01-15 07:32:10 -08:00
lateinit var snake : MagicSnakeLoader
2019-11-26 12:29:16 -08:00
override fun inflate ( inflater : LayoutInflater ) : FragmentHomeBinding =
FragmentHomeBinding . inflate ( inflater )
2019-12-23 11:19:47 -08:00
//
// LifeCycle
//
override fun onAttach ( context : Context ) {
twig ( " HomeFragment.onAttach " )
2020-01-15 07:32:10 -08:00
twig ( " ZZZ " )
twig ( " ZZZ " )
twig ( " ZZZ " )
twig ( " ZZZ ===================== HOME FRAGMENT CREATED ================================== " )
2019-12-23 11:19:47 -08:00
super . onAttach ( context )
2020-01-06 22:45:24 -08:00
// this will call startSync either now or later (after initializing with newly created seed)
2019-12-23 11:19:47 -08:00
walletSetup . checkSeed ( ) . onEach {
twig ( " Checking seed " )
2020-01-06 22:45:24 -08:00
if ( it == NO _SEED ) {
// interact with user to create, backup and verify seed
// leads to a call to startSync(), later (after accounts are created from seed)
twig ( " Seed not found, therefore, launching seed creation flow " )
mainActivity ?. navController ?. navigate ( R . id . action _nav _home _to _create _wallet )
} else {
twig ( " Found seed. Re-opening existing wallet " )
mainActivity ?. startSync ( walletSetup . openWallet ( ) )
2019-12-23 11:19:47 -08:00
}
} . launchIn ( lifecycleScope )
}
2019-11-26 12:29:16 -08:00
override fun onViewCreated ( view : View , savedInstanceState : Bundle ? ) {
super . onViewCreated ( view , savedInstanceState )
2019-12-23 11:19:47 -08:00
twig ( " HomeFragment.onViewCreated uiModel: ${::uiModel.isInitialized} saved: ${savedInstanceState != null} " )
2019-12-13 02:59:56 -08:00
with ( binding ) {
numberPad = arrayListOf (
2019-12-23 11:19:47 -08:00
buttonNumberPad0 . asKey ( ) ,
buttonNumberPad1 . asKey ( ) ,
buttonNumberPad2 . asKey ( ) ,
buttonNumberPad3 . asKey ( ) ,
buttonNumberPad4 . asKey ( ) ,
buttonNumberPad5 . asKey ( ) ,
buttonNumberPad6 . asKey ( ) ,
buttonNumberPad7 . asKey ( ) ,
buttonNumberPad8 . asKey ( ) ,
buttonNumberPad9 . asKey ( ) ,
buttonNumberPadDecimal . asKey ( ) ,
buttonNumberPadBack . asKey ( )
2019-12-13 02:59:56 -08:00
)
2020-01-09 08:02:47 -08:00
hitAreaReceive . onClickNavTo ( R . id . action _nav _home _to _nav _profile )
2019-12-13 02:59:56 -08:00
iconDetail . onClickNavTo ( R . id . action _nav _home _to _nav _detail )
textDetail . onClickNavTo ( R . id . action _nav _home _to _nav _detail )
2020-01-09 23:53:16 -08:00
hitAreaScan . setOnClickListener {
mainActivity ?. maybeOpenScan ( )
}
2019-11-27 06:24:00 -08:00
2019-12-13 02:59:56 -08:00
textBannerAction . setOnClickListener {
onBannerAction ( BannerAction . from ( ( it as ? TextView ) ?. text ?. toString ( ) ) )
}
2020-01-13 16:09:22 -08:00
buttonSendAmount . setOnClickListener {
2019-12-23 11:19:47 -08:00
onSend ( )
}
2020-01-09 23:53:16 -08:00
setSendAmount ( " 0 " , false )
2020-01-15 07:32:10 -08:00
snake = MagicSnakeLoader ( binding . lottieButtonLoading )
2019-12-23 11:19:47 -08:00
}
2020-01-09 08:02:47 -08:00
binding . buttonNumberPadBack . setOnLongClickListener {
onClearAmount ( )
true
}
2020-01-15 07:32:10 -08:00
if ( :: uiModel . isInitialized ) {
twig ( " uiModel exists! " )
onModelUpdated ( null , uiModel )
}
2019-12-23 11:19:47 -08:00
}
2020-01-09 08:02:47 -08:00
private fun onClearAmount ( ) {
2020-01-09 23:53:16 -08:00
if ( :: uiModel . isInitialized ) {
2020-01-09 08:02:47 -08:00
resumedScope . launch {
2020-01-09 23:53:16 -08:00
binding . textSendAmount . text . apply {
while ( uiModel . pendingSend != " 0 " ) {
2020-01-13 16:09:22 -08:00
viewModel . onChar ( '<' )
2020-01-09 23:53:16 -08:00
delay ( 5 )
}
}
2020-01-09 08:02:47 -08:00
}
}
}
2019-12-23 11:19:47 -08:00
override fun onResume ( ) {
super . onResume ( )
twig ( " HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope " )
2020-01-13 16:09:22 -08:00
viewModel . initializeMaybe ( )
2020-01-09 08:02:47 -08:00
onClearAmount ( )
2019-12-23 11:19:47 -08:00
viewModel . uiModels . scanReduce { old , new ->
onModelUpdated ( old , new )
new
2020-01-13 16:09:22 -08:00
} . onCompletion {
twig ( " uiModel.scanReduce completed. " )
2019-12-23 11:19:47 -08:00
} . catch { e ->
twig ( " exception while processing uiModels $e " )
2020-01-13 16:09:22 -08:00
throw e
2019-12-23 11:19:47 -08:00
} . launchIn ( resumedScope )
2019-12-13 02:59:56 -08:00
2019-12-23 11:19:47 -08:00
// TODO: see if there is a better way to trigger a refresh of the uiModel on resume
// the latest one should just be in the viewmodel and we should just "resubscribe"
// but for some reason, this doesn't always happen, which kind of defeats the purpose
// of having a cold stream in the view model
resumedScope . launch {
2020-01-06 22:45:24 -08:00
viewModel . refreshBalance ( )
2019-12-23 11:19:47 -08:00
}
2019-11-27 06:24:00 -08:00
}
2019-12-23 11:19:47 -08:00
override fun onSaveInstanceState ( outState : Bundle ) {
super . onSaveInstanceState ( outState )
twig ( " HomeFragment.onSaveInstanceState " )
if ( :: uiModel . isInitialized ) {
2020-01-13 16:09:22 -08:00
// outState.putParcelable("uiModel", uiModel)
2019-12-23 11:19:47 -08:00
}
}
2019-12-17 13:32:57 -08:00
2019-12-23 11:19:47 -08:00
override fun onViewStateRestored ( savedInstanceState : Bundle ? ) {
super . onViewStateRestored ( savedInstanceState )
savedInstanceState ?. let { inState ->
twig ( " HomeFragment.onViewStateRestored " )
2020-01-13 16:09:22 -08:00
// onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
2019-12-23 11:19:47 -08:00
}
}
//
// Public UI API
//
fun setSendEnabled ( enabled : Boolean ) {
2020-01-13 16:09:22 -08:00
binding . buttonSendAmount . apply {
2019-12-23 11:19:47 -08:00
isEnabled = enabled
2020-01-15 07:32:10 -08:00
if ( enabled ) {
// setTextColor(resources.getColorStateList(R.color.selector_button_text_dark))
binding . lottieButtonLoading . alpha = 1.0f
} else {
// setTextColor(R.color.zcashGray.toAppColor())
binding . lottieButtonLoading . alpha = 0.32f
}
2019-12-23 11:19:47 -08:00
}
}
2020-01-13 16:09:22 -08:00
fun setProgress ( uiModel : HomeViewModel . UiModel ) {
if ( ! uiModel . processorInfo . hasData ) {
twig ( " Warning: ignoring progress update because the processor has not started. " )
return
}
2020-01-15 07:32:10 -08:00
snake . isSynced = uiModel . isSynced
if ( ! uiModel . isSynced ) {
snake . downloadProgress = uiModel . downloadProgress
snake . scanProgress = uiModel . scanProgress
}
2020-01-13 16:09:22 -08:00
val sendText = when {
2020-01-15 07:32:10 -08:00
uiModel . isSynced -> if ( uiModel . hasFunds ) " SEND AMOUNT " else " NO FUNDS AVAILABLE "
2020-01-13 16:09:22 -08:00
uiModel . status == Synchronizer . Status . DISCONNECTED -> " DISCONNECTED "
uiModel . status == Synchronizer . Status . STOPPED -> " IDLE "
2020-01-15 07:32:10 -08:00
uiModel . isDownloading -> " Downloading . . . ${snake.downloadProgress} % "
2020-01-13 16:09:22 -08:00
uiModel . isValidating -> " Validating . . . "
2020-01-15 07:32:10 -08:00
uiModel . isScanning -> " Scanning . . . ${snake.scanProgress} % "
2020-01-13 16:09:22 -08:00
else -> " Updating "
2019-12-23 11:19:47 -08:00
}
2020-01-15 07:32:10 -08:00
// binding.lottieButtonLoading.progress = if (uiModel.isSynced) 1.0f else uiModel.totalProgress * 0.82f // line fully closes at 82% mark
2020-01-13 16:09:22 -08:00
binding . buttonSendAmount . text = sendText
2020-01-15 07:32:10 -08:00
// twig("Lottie progress set to ${binding.lottieButtonLoading.progress} (isSynced? ${uiModel.isSynced})")
2020-01-13 16:09:22 -08:00
twig ( " Send button set to: $sendText " )
val resId = if ( uiModel . isSynced ) R . color . selector _button _text _dark else R . color . selector _button _text _light
binding . buttonSendAmount . setTextColor ( resources . getColorStateList ( resId ) )
2020-01-15 07:32:10 -08:00
// if (uiModel.status == DISCONNECTED || uiModel.status == STOPPED) {
// binding.buttonSendAmount.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.zcashGray))
// } else {
// binding.buttonSendAmount.backgroundTintList = null
// }
2019-12-23 11:19:47 -08:00
}
2020-01-09 08:02:47 -08:00
/ * *
* @param amount the amount to send represented as ZEC , without the dollar sign .
* /
2020-01-09 23:53:16 -08:00
fun setSendAmount ( amount : String , updateModel : Boolean = true ) {
2020-01-09 08:02:47 -08:00
binding . textSendAmount . text = " \$ $amount " . toColoredSpan ( R . color . text _light _dimmed , " $ " )
2020-01-09 23:53:16 -08:00
if ( updateModel ) {
sendViewModel . zatoshiAmount = amount . safelyConvertToBigDecimal ( ) . convertZecToZatoshi ( )
}
2020-01-13 16:09:22 -08:00
binding . buttonSendAmount . disabledIf ( amount == " 0 " )
2019-12-23 11:19:47 -08:00
}
fun setAvailable ( availableBalance : Long = - 1L , totalBalance : Long = - 1L ) {
val availableString = if ( availableBalance < 0 ) " Updating " else availableBalance . convertZatoshiToZecString ( )
binding . textBalanceAvailable . text = availableString
binding . textBalanceDescription . apply {
goneIf ( availableBalance < 0 )
text = if ( availableBalance != - 1L && ( availableBalance < totalBalance ) ) {
2020-01-09 08:02:47 -08:00
val change = ( totalBalance - availableBalance ) . convertZatoshiToZecString ( )
2020-01-09 23:53:16 -08:00
" (expecting + $change ZEC) " . toColoredSpan ( R . color . text _light , " + $change " )
2019-12-23 11:19:47 -08:00
} else {
" (enter an amount to send) "
}
}
}
fun setBanner ( message : String = " " , action : BannerAction = CLEAR ) {
with ( binding ) {
val hasMessage = ! message . isEmpty ( ) || action != CLEAR
groupBalance . goneIf ( hasMessage )
groupBanner . goneIf ( ! hasMessage )
layerLock . goneIf ( ! hasMessage )
textBannerMessage . text = message
textBannerAction . text = action . action
}
}
//
// Private UI Events
//
2020-01-15 07:32:10 -08:00
private fun onModelUpdated ( old : HomeViewModel . UiModel ? , new : HomeViewModel . UiModel ) {
2020-01-13 16:09:22 -08:00
twig ( " onModelUpdated: $new " )
2020-01-15 08:27:09 -08:00
if ( binding . lottieButtonLoading . visibility != View . VISIBLE ) binding . lottieButtonLoading . visibility = View . VISIBLE
2019-12-23 11:19:47 -08:00
uiModel = new
2020-01-15 07:32:10 -08:00
if ( old ?. pendingSend != new . pendingSend ) {
2019-12-23 11:19:47 -08:00
setSendAmount ( new . pendingSend )
}
// TODO: handle stopped and disconnected flows
2020-01-13 16:09:22 -08:00
setProgress ( uiModel ) // TODO: we may not need to separate anymore
2020-01-15 07:32:10 -08:00
// if (new.status = SYNCING) onSyncing(new) else onSynced(new)
if ( new . status == SYNCED ) onSynced ( new ) else onSyncing ( new )
2019-12-23 11:19:47 -08:00
setSendEnabled ( new . isSendEnabled )
}
private fun onSyncing ( uiModel : HomeViewModel . UiModel ) {
setAvailable ( )
}
private fun onSynced ( uiModel : HomeViewModel . UiModel ) {
2020-01-15 07:32:10 -08:00
snake . isSynced = true
2020-01-09 08:02:47 -08:00
if ( ! uiModel . hasBalance ) {
2019-12-23 11:19:47 -08:00
onNoFunds ( )
} else {
setBanner ( " " )
setAvailable ( uiModel . availableBalance , uiModel . totalBalance )
}
}
private fun onSend ( ) {
mainActivity ?. navController ?. navigate ( R . id . action _nav _home _to _send )
2019-12-17 13:32:57 -08:00
}
2019-11-27 06:24:00 -08:00
private fun onBannerAction ( action : BannerAction ) {
when ( action ) {
2019-12-23 11:19:47 -08:00
FUND _NOW -> {
2019-11-27 06:24:00 -08:00
MaterialAlertDialogBuilder ( activity )
2020-01-13 17:58:09 -08:00
. setMessage ( " To make full use of this wallet, deposit funds to your address. " )
2019-11-27 06:24:00 -08:00
. setTitle ( " No Balance " )
. setCancelable ( true )
2020-01-13 17:58:09 -08:00
. setPositiveButton ( " View Address " ) { dialog , _ ->
2019-11-27 06:24:00 -08:00
dialog . dismiss ( )
mainActivity ?. navController ?. navigate ( R . id . action _nav _home _to _nav _receive )
}
. show ( )
2020-01-13 17:58:09 -08:00
// MaterialAlertDialogBuilder(activity)
// .setMessage("To make full use of this wallet, deposit funds to your address or tap the faucet to trigger a tiny automatic deposit.\n\nFaucet funds are made available for the community by the community for testing. So please be kind enough to return what you borrow!")
// .setTitle("No Balance")
// .setCancelable(true)
// .setPositiveButton("Tap Faucet") { dialog, _ ->
// dialog.dismiss()
// setBanner("Tapping faucet...", CANCEL)
// }
// .setNegativeButton("View Address") { dialog, _ ->
// dialog.dismiss()
// mainActivity?.navController?.navigate(R.id.action_nav_home_to_nav_receive)
// }
// .show()
2019-11-27 06:24:00 -08:00
}
CANCEL -> {
// TODO: trigger banner / balance update
onNoFunds ( )
}
}
}
private fun onNoFunds ( ) {
2019-12-23 11:19:47 -08:00
setBanner ( " No Balance " , FUND _NOW )
2019-11-27 06:24:00 -08:00
}
2019-12-23 11:19:47 -08:00
//
// Inner classes and extensions
//
2019-11-27 06:24:00 -08:00
enum class BannerAction ( val action : String ) {
2020-01-31 08:32:36 -08:00
FUND _NOW ( " " ) ,
2019-11-27 06:24:00 -08:00
CANCEL ( " Cancel " ) ,
2019-12-23 11:19:47 -08:00
NONE ( " " ) ,
CLEAR ( " clear " ) ;
2019-11-27 06:24:00 -08:00
companion object {
fun from ( action : String ? ) : BannerAction {
values ( ) . forEach {
if ( it . action == action ) return it
}
throw IllegalArgumentException ( " Invalid BannerAction: $action " )
}
2019-11-26 12:29:16 -08:00
}
}
2019-12-23 11:19:47 -08:00
private fun TextView . asKey ( ) : TextView {
val c = text [ 0 ]
setOnClickListener {
lifecycleScope . launch {
twig ( " CHAR TYPED: $c " )
2020-01-13 16:09:22 -08:00
viewModel . onChar ( c )
2019-12-23 11:19:47 -08:00
}
}
return this
}
// TODO: remove these troubleshooting logs
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
twig ( " HomeFragment.onCreate " )
}
override fun onActivityCreated ( savedInstanceState : Bundle ? ) {
super . onActivityCreated ( savedInstanceState )
twig ( " HomeFragment.onActivityCreated " )
}
override fun onStart ( ) {
super . onStart ( )
twig ( " HomeFragment.onStart " )
}
override fun onPause ( ) {
super . onPause ( )
twig ( " HomeFragment.onPause resumeScope.isActive: ${resumedScope.isActive} " )
}
override fun onStop ( ) {
super . onStop ( )
twig ( " HomeFragment.onStop " )
}
override fun onDestroyView ( ) {
super . onDestroyView ( )
twig ( " HomeFragment.onDestroyView " )
}
override fun onDestroy ( ) {
super . onDestroy ( )
twig ( " HomeFragment.onDestroy " )
}
override fun onDetach ( ) {
super . onDetach ( )
twig ( " HomeFragment.onDetach " )
}
2019-11-26 12:29:16 -08:00
}