2020-01-31 08:31:36 -08:00
|
|
|
package cash.z.ecc.android.ui.setup
|
|
|
|
|
|
|
|
import android.graphics.drawable.Drawable
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.SystemClock
|
|
|
|
import android.text.InputType
|
2020-02-12 04:55:44 -08:00
|
|
|
import android.view.KeyEvent
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.MotionEvent
|
2020-01-31 08:31:36 -08:00
|
|
|
import android.view.MotionEvent.ACTION_DOWN
|
|
|
|
import android.view.MotionEvent.ACTION_UP
|
2020-02-12 04:55:44 -08:00
|
|
|
import android.view.View
|
2020-01-31 08:31:36 -08:00
|
|
|
import android.widget.TextView
|
|
|
|
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
|
2020-06-10 05:26:21 -07:00
|
|
|
import cash.z.ecc.android.ext.showInvalidSeedPhraseError
|
2020-02-21 15:52:57 -08:00
|
|
|
import cash.z.ecc.android.feedback.Report
|
|
|
|
import cash.z.ecc.android.feedback.Report.Funnel.Restore
|
|
|
|
import cash.z.ecc.android.feedback.Report.Tap.*
|
2020-01-31 08:31:36 -08:00
|
|
|
import cash.z.ecc.android.ui.base.BaseFragment
|
2020-06-10 04:49:38 -07:00
|
|
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
|
|
|
import cash.z.ecc.android.sdk.ext.twig
|
2020-01-31 08:31:36 -08:00
|
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
|
|
import com.tylersuehr.chips.Chip
|
|
|
|
import com.tylersuehr.chips.ChipsAdapter
|
|
|
|
import com.tylersuehr.chips.SeedWordAdapter
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
|
|
|
|
|
|
|
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
|
2020-02-21 15:52:57 -08:00
|
|
|
override val screen = Report.Screen.RESTORE
|
2020-01-31 08:31:36 -08:00
|
|
|
|
|
|
|
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
|
|
|
|
|
|
|
|
private lateinit var seedWordRecycler: RecyclerView
|
|
|
|
private var seedWordAdapter: SeedWordAdapter? = null
|
|
|
|
|
|
|
|
override fun inflate(inflater: LayoutInflater): FragmentRestoreBinding =
|
|
|
|
FragmentRestoreBinding.inflate(inflater)
|
|
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
|
|
|
seedWordRecycler = binding.chipsInput.findViewById<RecyclerView>(R.id.chips_recycler)
|
|
|
|
seedWordAdapter = SeedWordAdapter(seedWordRecycler.adapter as ChipsAdapter).onDataSetChanged {
|
|
|
|
onChipsModified()
|
|
|
|
}.also { onChipsModified() }
|
|
|
|
seedWordRecycler.adapter = seedWordAdapter
|
|
|
|
|
|
|
|
|
|
|
|
binding.chipsInput.apply {
|
|
|
|
setFilterableChipList(getChips())
|
|
|
|
setDelimiter("[ ;,]", true)
|
|
|
|
}
|
|
|
|
|
|
|
|
binding.buttonDone.setOnClickListener {
|
2020-02-21 15:52:57 -08:00
|
|
|
onDone().also { tapped(RESTORE_DONE) }
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
binding.buttonSuccess.setOnClickListener {
|
2020-02-21 15:52:57 -08:00
|
|
|
onEnterWallet().also { tapped(RESTORE_SUCCESS) }
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
|
|
super.onActivityCreated(savedInstanceState)
|
|
|
|
mainActivity?.onFragmentBackPressed(this) {
|
2020-02-21 15:52:57 -08:00
|
|
|
tapped(RESTORE_BACK)
|
2020-01-31 08:31:36 -08:00
|
|
|
if (seedWordAdapter == null || seedWordAdapter?.itemCount == 1) {
|
|
|
|
onExit()
|
|
|
|
} else {
|
|
|
|
MaterialAlertDialogBuilder(activity)
|
|
|
|
.setMessage("Are you sure? For security, the words that you have entered will be cleared!")
|
|
|
|
.setTitle("Abort?")
|
|
|
|
.setPositiveButton("Stay") { dialog, _ ->
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.Stay)
|
2020-01-31 08:31:36 -08:00
|
|
|
dialog.dismiss()
|
|
|
|
}
|
|
|
|
.setNegativeButton("Exit") { dialog, _ ->
|
|
|
|
dialog.dismiss()
|
|
|
|
onExit()
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
// Require one less tap to enter the seed words
|
|
|
|
touchScreenForUser()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun onExit() {
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.Exit)
|
2020-01-31 08:31:36 -08:00
|
|
|
hideAutoCompleteWords()
|
2020-02-12 04:55:44 -08:00
|
|
|
mainActivity?.hideKeyboard()
|
2020-01-31 08:31:36 -08:00
|
|
|
mainActivity?.navController?.popBackStack()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onEnterWallet() {
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.Success)
|
2020-02-12 04:55:44 -08:00
|
|
|
mainActivity?.safeNavigate(R.id.action_nav_restore_to_nav_home)
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun onDone() {
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.Done)
|
2020-02-12 04:55:44 -08:00
|
|
|
mainActivity?.hideKeyboard()
|
2020-01-31 08:31:36 -08:00
|
|
|
val seedPhrase = binding.chipsInput.selectedChips.joinToString(" ") {
|
|
|
|
it.title
|
|
|
|
}
|
|
|
|
var birthday = binding.root.findViewById<TextView>(R.id.input_birthdate).text.toString()
|
|
|
|
.let { birthdateString ->
|
|
|
|
if (birthdateString.isNullOrEmpty()) ZcashSdk.SAPLING_ACTIVATION_HEIGHT else birthdateString.toInt()
|
|
|
|
}.coerceAtLeast(ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
|
|
|
|
|
2020-06-10 05:26:21 -07:00
|
|
|
try {
|
|
|
|
walletSetup.validatePhrase(seedPhrase)
|
|
|
|
importWallet(seedPhrase, birthday)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
mainActivity?.showInvalidSeedPhraseError(t)
|
|
|
|
}
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun importWallet(seedPhrase: String, birthday: Int) {
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.ImportStarted)
|
2020-02-12 04:55:44 -08:00
|
|
|
mainActivity?.hideKeyboard()
|
2020-01-31 08:31:36 -08:00
|
|
|
mainActivity?.apply {
|
|
|
|
lifecycleScope.launch {
|
|
|
|
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
|
2020-02-12 04:55:44 -08:00
|
|
|
// bugfix: if the user proceeds before the synchronizer is created the app will crash!
|
|
|
|
binding.buttonSuccess.isEnabled = true
|
2020-02-21 15:52:57 -08:00
|
|
|
mainActivity?.reportFunnel(Restore.ImportCompleted)
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
playSound("sound_receive_small.mp3")
|
|
|
|
vibrateSuccess()
|
|
|
|
}
|
|
|
|
|
|
|
|
binding.groupDone.visibility = View.GONE
|
|
|
|
binding.groupStart.visibility = View.GONE
|
|
|
|
binding.groupSuccess.visibility = View.VISIBLE
|
2020-02-12 04:55:44 -08:00
|
|
|
binding.buttonSuccess.isEnabled = false
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun onChipsModified() {
|
|
|
|
seedWordAdapter?.editText?.apply {
|
|
|
|
postDelayed({
|
|
|
|
requestFocus()
|
|
|
|
},40L)
|
|
|
|
}
|
|
|
|
setDoneEnabled()
|
2020-02-12 04:55:44 -08:00
|
|
|
|
|
|
|
view!!.postDelayed({
|
|
|
|
mainActivity!!.showKeyboard(seedWordAdapter!!.editText)
|
|
|
|
seedWordAdapter?.editText?.requestFocus()
|
|
|
|
}, 500L)
|
2020-01-31 08:31:36 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun setDoneEnabled() {
|
|
|
|
val count = seedWordAdapter?.itemCount ?: 0
|
2020-02-21 15:52:57 -08:00
|
|
|
reportWords(count - 1) // subtract 1 for the editText
|
2020-01-31 08:31:36 -08:00
|
|
|
binding.groupDone.goneIf(count <= 24)
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:52:57 -08:00
|
|
|
private fun reportWords(count: Int) {
|
|
|
|
mainActivity?.run {
|
|
|
|
// reportFunnel(Restore.SeedWordCount(count))
|
|
|
|
if (count == 1) {
|
|
|
|
reportFunnel(Restore.SeedWordsStarted)
|
|
|
|
} else if (count == 24) {
|
|
|
|
reportFunnel(Restore.SeedWordsCompleted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-31 08:31:36 -08:00
|
|
|
private fun hideAutoCompleteWords() {
|
|
|
|
seedWordAdapter?.editText?.setText("")
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getChips(): List<Chip> {
|
|
|
|
return resources.getStringArray(R.array.word_list).map {
|
|
|
|
SeedWordChip(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun touchScreenForUser() {
|
|
|
|
seedWordAdapter?.editText?.apply {
|
|
|
|
postDelayed({
|
|
|
|
seedWordAdapter?.editText?.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
|
|
dispatchTouchEvent(motionEvent(ACTION_DOWN))
|
|
|
|
dispatchTouchEvent(motionEvent(ACTION_UP))
|
|
|
|
}, 100L)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun motionEvent(action: Int) = SystemClock.uptimeMillis().let { now ->
|
|
|
|
MotionEvent.obtain(now, now, action, 0f, 0f, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class SeedWordChip(val word: String, var index: Int = -1) : Chip() {
|
|
|
|
override fun getSubtitle(): String? = null//"subtitle for $word"
|
|
|
|
override fun getAvatarDrawable(): Drawable? = null
|
|
|
|
override fun getId() = index
|
|
|
|
override fun getTitle() = word
|
|
|
|
override fun getAvatarUri() = null
|
|
|
|
}
|