zcash-android-wallet/app/src/main/java/cash/z/ecc/android/ui/setup/BackupFragment.kt

143 lines
6.0 KiB
Kotlin

package cash.z.ecc.android.ui.setup
import android.content.Context
import android.os.Bundle
import android.text.SpannableString
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBackupBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_VERIFY
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.RuntimeException
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override val screen = Report.Screen.BACKUP
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var hasBackUp: Boolean = true // TODO: implement backup and then check for it here-ish
override fun inflate(inflater: LayoutInflater): FragmentBackupBinding =
FragmentBackupBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
applySpan(
textAddressPart1, textAddressPart2, textAddressPart3,
textAddressPart4, textAddressPart5, textAddressPart6,
textAddressPart7, textAddressPart8, textAddressPart9,
textAddressPart10, textAddressPart11, textAddressPart12,
textAddressPart13, textAddressPart14, textAddressPart15,
textAddressPart16, textAddressPart17, textAddressPart18,
textAddressPart19, textAddressPart20, textAddressPart21,
textAddressPart22, textAddressPart23, textAddressPart24
)
}
binding.buttonPositive.setOnClickListener {
onEnterWallet().also { if (hasBackUp) tapped(BACKUP_DONE) else tapped(BACKUP_VERIFY) }
}
if (hasBackUp) {
binding.buttonPositive.text = getString(R.string.backup_button_done)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity?.onBackPressedDispatcher?.addCallback(this) {
onEnterWallet(false)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
hasBackUp = when (it) {
SEED_WITH_BACKUP -> true
else -> false
}
}.launchIn(lifecycleScope)
}
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text = getString(R.string.backup_format_birthday_height, calculateBirthday())
}
}
// TODO: move this into the SDK
private suspend fun calculateBirthday(): Int {
var storedBirthday = 0
var oldestTransactionHeight = 0
var activationHeight = 0
try {
activationHeight = mainActivity?.synchronizerComponent?.synchronizer()?.network?.saplingActivationHeight ?: 0
storedBirthday = walletSetup.loadBirthdayHeight() ?: 0
oldestTransactionHeight = mainActivity?.synchronizerComponent?.synchronizer()?.receivedTransactions?.first()?.last()?.minedHeight ?: 0
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away
oldestTransactionHeight = ZcashSdk.MAX_REORG_SIZE.let { boundary ->
oldestTransactionHeight.let { it - it.rem(boundary) - boundary }
}
} catch (t: Throwable) {
twig("failed to calculate birthday due to: $t")
}
return maxOf(storedBirthday, oldestTransactionHeight, activationHeight)
}
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(activity, R.string.backup_verification_not_implemented, Toast.LENGTH_LONG).show()
}
mainActivity?.navController?.popBackStack()
}
private fun applySpan(vararg textViews: TextView) = lifecycleScope.launch {
val words = loadSeedWords()
val thinSpace = "\u2005" // 0.25 em space
textViews.forEachIndexed { index, textView ->
val numLength = "$index".length
val word = words[index]
// TODO: work with a charsequence here, rather than constructing a String
textView.text = SpannableString("${index + 1}$thinSpace${String(word)}").apply {
setSpan(AddressPartNumberSpan(), 0, 1 + numLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
private suspend fun loadSeedWords(): List<CharArray> = withContext(Dispatchers.IO) {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase)
result
}
}
}