Restore feature cleanup.

Show birthdate in backup fragment.
This commit is contained in:
Kevin Gorham 2020-02-12 07:55:44 -05:00
parent a357afe09a
commit 6da700d683
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
11 changed files with 100 additions and 713 deletions

View File

@ -76,7 +76,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
}.launchIn(lifecycleScope)
}
private fun onEnterWallet(showMessage: Boolean = this.hasBackUp != true) {
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text = "Birthday Height: %,d".format(walletSetup.loadBirthdayHeight())
}
}
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()
}

View File

@ -1,37 +1,26 @@
package cash.z.ecc.android.ui.setup
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.SystemClock
import android.text.Editable
import android.text.InputType
import android.view.*
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.view.inputmethod.InputMethodManager
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentRestoreBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toPx
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.chip.ChipCreator
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
import com.tylersuehr.chips.Chip
import com.tylersuehr.chips.ChipsAdapter
import com.tylersuehr.chips.SeedWordAdapter
@ -70,13 +59,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
binding.buttonSuccess.setOnClickListener {
onEnterWallet()
}
//
//
// seedWordAdapter!!.editText.setOnKeyListener(this)
binding.textTitle.setOnClickListener {
seedWordAdapter!!.editText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_CLASS_NUMBER
}
binding.textSubtitle.setOnClickListener {
seedWordAdapter!!.editText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
@ -113,16 +95,16 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
private fun onExit() {
hideAutoCompleteWords()
setKeyboardShown(false)
mainActivity?.hideKeyboard()
mainActivity?.navController?.popBackStack()
}
private fun onEnterWallet() {
mainActivity?.navController?.navigate(R.id.action_nav_restore_to_nav_home)
mainActivity?.safeNavigate(R.id.action_nav_restore_to_nav_home)
}
private fun onDone() {
setKeyboardShown(false)
mainActivity?.hideKeyboard()
val seedPhrase = binding.chipsInput.selectedChips.joinToString(" ") {
it.title
}
@ -135,10 +117,12 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
}
private fun importWallet(seedPhrase: String, birthday: Int) {
setKeyboardShown(false)
mainActivity?.hideKeyboard()
mainActivity?.apply {
lifecycleScope.launch {
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
// bugfix: if the user proceeds before the synchronizer is created the app will crash!
binding.buttonSuccess.isEnabled = true
}
playSound("sound_receive_small.mp3")
vibrateSuccess()
@ -147,16 +131,22 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
binding.groupDone.visibility = View.GONE
binding.groupStart.visibility = View.GONE
binding.groupSuccess.visibility = View.VISIBLE
binding.buttonSuccess.isEnabled = false
}
private fun onChipsModified() {
twig("onChipsModified")
seedWordAdapter?.editText?.apply {
postDelayed({
requestFocus()
isCursorVisible = false
},40L)
}
setDoneEnabled()
view!!.postDelayed({
mainActivity!!.showKeyboard(seedWordAdapter!!.editText)
seedWordAdapter?.editText?.requestFocus()
}, 500L)
}
private fun setDoneEnabled() {
@ -164,14 +154,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
binding.groupDone.goneIf(count <= 24)
}
private fun setKeyboardShown(isShown: Boolean) {
if (isShown) {
mainActivity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
} else {
(requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view!!.windowToken, 0);
}
}
private fun hideAutoCompleteWords() {
seedWordAdapter?.editText?.setText("")
}
@ -202,108 +184,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
}
class SeedWordTokenizer(context: Context) : SpanChipTokenizer<ChipSpan>(context, MyChipSpanChipCreator(), ChipSpan::class.java) {
override fun applyConfiguration(text: Editable, chipConfiguration: ChipConfiguration) {
mChipConfiguration = chipConfiguration
val allChips = findAllChips(0, text.length, text)
allChips.forEachIndexed { i, chip ->
val chipStart = findChipStart(chip, text)
deleteChip(chip, text)
val newChip = (mChipCreator as MyChipSpanChipCreator).createChip(mContext, chip, i)
text.insert(chipStart, terminateToken(newChip))
}
}
}
class MyChipSpanChipCreator : ChipCreator<ChipSpan> {
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan {
return MyChipSpan(context, text, ContextCompat.getDrawable(context, R.mipmap.ic_launcher), data)
}
fun createChip(context: Context, existingChip: ChipSpan, data: Any?): MyChipSpan {
return MyChipSpan(context, existingChip, data)
}
override fun createChip(context: Context, existingChip: ChipSpan): ChipSpan {
throw IllegalAccessException("Provide data when creating a chip")
}
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
val chipHorizontalSpacing = chipConfiguration.chipHorizontalSpacing
val chipBackground = chipConfiguration.chipBackground
val chipCornerRadius = chipConfiguration.chipCornerRadius
val chipTextColor = chipConfiguration.chipTextColor
val chipTextSize = chipConfiguration.chipTextSize
val chipHeight = chipConfiguration.chipHeight
val chipVerticalSpacing = chipConfiguration.chipVerticalSpacing
val maxAvailableWidth = chipConfiguration.maxAvailableWidth
if (chipHorizontalSpacing != -1) {
chip.setLeftMargin(chipHorizontalSpacing / 2)
chip.setRightMargin(chipHorizontalSpacing / 2)
}
if (chipBackground != null) {
chip.setBackgroundColor(chipBackground)
}
if (chipCornerRadius != -1) {
chip.setCornerRadius(chipCornerRadius)
}
if (chipTextColor != Color.TRANSPARENT) {
chip.setTextColor(chipTextColor)
}
if (chipTextSize != -1) {
chip.setTextSize(chipTextSize)
}
if (chipHeight != -1) {
chip.setChipHeight(chipHeight)
}
if (chipVerticalSpacing != -1) {
chip.setChipVerticalSpacing(chipVerticalSpacing)
}
if (maxAvailableWidth != -1) {
chip.setMaxAvailableWidth(maxAvailableWidth)
}
chip.setShowIconOnLeft(true)
}
}
class MyChipSpan : ChipSpan {
val index: Int
constructor(context: Context, text: CharSequence, drawable: Drawable?, data: Any?)
: super(context, text, drawable, data) {
index = data as? Int ?: 0
}
constructor(context: Context, chip: ChipSpan, data: Any?)
: super(context, chip) {
index = data as? Int ?: 0
}
override fun drawBackground(canvas: Canvas, x: Float, top: Int, bottom: Int, paint: Paint) {
val rect = RectF(x, top.toFloat(), x + mChipWidth, bottom.toFloat())
val cornerRadius = 4.0f.toPx()
paint.color = R.color.background_banner.toAppColor()
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
paint.color = R.color.background_banner_stroke.toAppColor()
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeMiter = 10.0f
paint.strokeWidth = 1.5f.toPx()
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
paint.style = Paint.Style.FILL
paint.color = mTextColor
}
}
class SeedWordChip(val word: String, var index: Int = -1) : Chip() {
override fun getSubtitle(): String? = null//"subtitle for $word"
override fun getAvatarDrawable(): Drawable? = null

View File

@ -7,6 +7,8 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.setup.SeedWordChip
import cash.z.wallet.sdk.ext.twig
class SeedWordAdapter : ChipsAdapter {
@ -40,6 +42,11 @@ class SeedWordAdapter : ChipsAdapter {
"${size + 1}"
}
}
}
override fun onChipDataSourceChanged() {
super.onChipDataSourceChanged()
twig("onChipDataSourceChanged")
onDataSetChangedListener?.invoke()
}
@ -52,8 +59,21 @@ class SeedWordAdapter : ChipsAdapter {
if (TextUtils.isEmpty(text)) return
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mEditText.setText("");
mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply {
postDelayed({
setText("")
requestFocus()
}, 50L)
}
}
}
override fun onKeyboardDelimiter(text: String) {
twig("onKeyboardDelimiter: $text ${mDataSource.filteredChips.size}")
if (mDataSource.filteredChips.size > 0) {
onKeyboardActionDone((mDataSource.filteredChips.first() as SeedWordChip).word)
}
}

View File

@ -3,13 +3,15 @@ package cash.z.ecc.android.ui.setup
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore
import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.ImportedWalletBirthdayStore
import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.NewWalletBirthdayStore
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -47,26 +49,29 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
*/
fun openWallet(): Initializer {
twig("Opening existing wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().open(birthdayStore().getBirthday())
}
return ZcashWalletApp.component.initializerSubcomponent()
.create(DefaultBirthdayStore(ZcashWalletApp.instance)).run {
initializer().open(birthdayStore().getBirthday())
}
}
suspend fun newWallet(): Initializer {
twig("Initializing new wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().apply {
new(createWallet(), birthdayStore().newWalletBirthday)
return ZcashWalletApp.component.initializerSubcomponent()
.create(NewWalletBirthdayStore(ZcashWalletApp.instance)).run {
initializer().apply {
new(createWallet(), birthdayStore().getBirthday())
}
}
}
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
twig("Importing wallet. Requested birthday: $birthdayHeight")
return ZcashWalletApp.component.initializerSubcomponent().create(Initializer.DefaultBirthdayStore(ZcashWalletApp.instance, birthdayHeight)).run {
initializer().apply {
import(importWallet(seedPhrase.toCharArray()), birthdayStore().getBirthday())
}
return ZcashWalletApp.component.initializerSubcomponent()
.create(ImportedWalletBirthdayStore(ZcashWalletApp.instance, birthdayHeight)).run {
initializer().apply {
import(importWallet(seedPhrase.toCharArray()), birthdayStore().getBirthday())
}
}
}
@ -75,7 +80,7 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
*
* @param feedback the object used for measurement.
*/
private suspend fun createWallet(): ByteArray = withContext(Dispatchers.IO){
private suspend fun createWallet(): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot create a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
@ -84,7 +89,8 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
feedback.measure(WALLET_CREATED) {
mnemonics.run {
feedback.measure(ENTROPY_CREATED) { nextEntropy() }.let { entropy ->
feedback.measure(SEED_PHRASE_CREATED) { nextMnemonic(entropy) }.let { seedPhrase ->
feedback.measure(SEED_PHRASE_CREATED) { nextMnemonic(entropy) }
.let { seedPhrase ->
feedback.measure(SEED_CREATED) { toSeed(seedPhrase) }.let { bip39Seed ->
lockBox.setCharsUtf8(LockBoxKey.SEED_PHRASE, seedPhrase)
@ -101,6 +107,12 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
}
}
suspend fun loadBirthdayHeight(): Int = withContext(Dispatchers.IO) {
DefaultBirthdayStore(ZcashWalletApp.instance).getBirthday().height
}
/**
* Take all the steps necessary to import a wallet and measure how long it takes.
*

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:color="@color/text_light"/>
<item android:state_pressed="true" android:color="@color/text_light_dimmed" />
</selector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/zcashGray"/>
<corners android:radius="2dp"/>
</shape>

File diff suppressed because one or more lines are too long

View File

@ -22,11 +22,10 @@
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:padding="16dp"
android:textSize="18dp"
android:includeFontPadding="false"
android:textColor="@color/text_light"
android:textColor="@color/selector_button_text_light_to_dimmed"
app:layout_constraintBottom_toTopOf="@+id/subtitle"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"

View File

@ -317,6 +317,19 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
app:constraint_referenced_ids="text_address_part_2, text_address_part_5, text_address_part_8, text_address_part_11, text_address_part_14, text_address_part_17, text_address_part_20, text_address_part_23" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_birtdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Birthday Height: 510,123"
android:textSize="20dp"
android:fontFamily="@font/inconsolata"
app:layout_constraintTop_toBottomOf="@id/receive_address_parts"
app:layout_constraintBottom_toTopOf="@id/text_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -1,16 +0,0 @@
package cash.z.ecc.android.lockbox
/**
* Generic interface to separate the underlying implementation used by this module and the code that
* interacts with it.
*/
interface LockBoxProvider {
fun setBytes(key: String, value: ByteArray)
fun getBytes(key: String): ByteArray?
fun setCharsUtf8(key: String, value: CharArray)
fun getCharsUtf8(key: String): CharArray?
fun setBoolean(key: String, value: Boolean)
fun getBoolean(key: String): Boolean
}

View File

@ -1,44 +0,0 @@
package cash.z.ecc.kotlin.mnemonic
/**
* Generic interface to separate the underlying implementation used by this module and the code that
* interacts with it.
*/
interface MnemonicProvider {
/**
* Generate a random seed.
*/
fun nextEntropy(): ByteArray
/**
* Generate a random 24-word mnemonic phrase.
*/
fun nextMnemonic(): CharArray
/**
* Generate the 24-word mnemonic phrase corresponding to the given seed.
*/
fun nextMnemonic(seed: ByteArray): CharArray
/**
* Generate a random 24-word mnemonic phrase, represented as a list of words.
*/
fun nextMnemonicList(): List<CharArray>
/**
* Generate the 24-word mnemonic phrase corresponding to the given seed, represented as a list.
*/
fun nextMnemonicList(seed: ByteArray): List<CharArray>
/**
* Generate a 64-byte seed from the 24-word mnemonic phrase.
*/
fun toSeed(mnemonic: CharArray): ByteArray
/**
* Split the given mnemonic around spaces.
*/
fun toWordList(mnemonic: CharArray): List<CharArray>
}