Merge pull request #40 from zcash/sprint/49

Sprint/49
This commit is contained in:
Kevin Gorham 2019-12-23 14:42:22 -05:00 committed by GitHub
commit 5cdfc97945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1828 additions and 283 deletions

View File

@ -8,7 +8,7 @@ apply plugin: 'kotlin-kapt'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha03'
version = '1.0.0-alpha04'
android {
compileSdkVersion Deps.compileSdkVersion
@ -18,10 +18,12 @@ android {
applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_003
versionCode = 1_00_00_004
// last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionName = "$version"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
}
flavorDimensions 'network'
productFlavors {
@ -40,11 +42,13 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled true
minifyEnabled false
shrinkResources false
useProguard false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@ -82,6 +86,7 @@ dependencies {
implementation project(':feedback')
implementation project(':mnemonic')
implementation project(':lockbox')
implementation project(':sdk')
// Kotlin
implementation Deps.Kotlin.STDLIB
@ -102,6 +107,11 @@ dependencies {
kapt Deps.Dagger.ANDROID_PROCESSOR
kapt Deps.Dagger.COMPILER
// Testing these BIP39 dependencies
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'io.github.novacrypto:BIP39:2019.01.27'
implementation 'io.github.novacrypto:securestring:2019.01.27'
// grpc-java
implementation "io.grpc:grpc-okhttp:1.21.0"
implementation "io.grpc:grpc-android:1.21.0"

View File

@ -1,6 +1,12 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
# Reports
-printusage build/outputs/logs/R8-removed-code-report.txt
-printseeds build/outputs/logs/R8-entry-points-report.txt
## Okio
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn org.codehaus.mojo.animal_sniffer.*
#-keep class cash.z.** { *; }

View File

@ -3,16 +3,116 @@ package cash.z.ecc.android.integration
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.wallet.sdk.Initializer
import okio.Buffer
import okio.GzipSink
import okio.Okio
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class IntegrationTest {
private lateinit var appContext: Context
private val mnemonics = Mnemonics()
private val phrase =
"human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor" +
" under absorb spirit hurdle animal original honey owner upper empower describe"
@Before
fun start() {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
}
@Test
fun testSeed_generation() {
val seed = mnemonics.toSeed(phrase.toCharArray())
assertEquals(
"Generated incorrect BIP-39 seed!",
"f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" +
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
seed.toHex()
)
}
@Test
fun testSeed_storage() {
val seed = mnemonics.toSeed(phrase.toCharArray())
val lb = LockBox(appContext)
lb.setBytes("seed", seed)
assertTrue(seed.contentEquals(lb.getBytes("seed")!!))
}
@Test
fun testPhrase_storage() {
val lb = LockBox(appContext)
val phraseChars = phrase.toCharArray()
lb.setCharsUtf8("phrase", phraseChars)
assertTrue(phraseChars.contentEquals(lb.getCharsUtf8("phrase")!!))
}
@Test
fun testPhrase_maxLengthStorage() {
val lb = LockBox(appContext)
// find and expose the max length
var acceptedSize = 256
while (acceptedSize > 0) {
try {
lb.setCharsUtf8("temp", nextString(acceptedSize).toCharArray())
break
} catch (t: Throwable) {
}
acceptedSize--
}
val maxSeedPhraseLength = 8 * 24 + 23 //215 (max length of each word is 8)
assertTrue(
"LockBox does not support the maximum length seed phrase." +
" Expected: $maxSeedPhraseLength but was: $acceptedSize",
acceptedSize > maxSeedPhraseLength
)
}
@Test
fun testAddress() {
val seed = mnemonics.toSeed(phrase.toCharArray())
val initializer = Initializer(appContext).apply {
new(seed, overwrite = true)
}
assertEquals(
"Generated incorrect z-address!",
"zs1gn2ah0zqhsxnrqwuvwmgxpl5h3ha033qexhsz8tems53fw877f4gug353eefd6z8z3n4zxty65c",
initializer.rustBackend.getAddress()
)
initializer.clear()
}
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)
sb.append(String.format("%02x", b))
return sb.toString()
}
fun String.gzip(): ByteArray {
val result = Buffer()
val sink = Okio.buffer(GzipSink(result))
sink.use {
sink.write(toByteArray())
}
return result.readByteArray()
}
fun nextString(length: Int): String {
val allowedChars = "ACGT"
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
}

View File

@ -4,6 +4,9 @@ import android.content.Context
import android.os.Build
import cash.z.ecc.android.di.DaggerAppComponent
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import dagger.android.AndroidInjector
import dagger.android.DaggerApplication
import javax.inject.Inject
@ -23,7 +26,7 @@ class ZcashWalletApp : DaggerApplication() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
// Twig.plant(TroubleshootingTwig())
Twig.plant(TroubleshootingTwig())
}
/**
@ -46,6 +49,7 @@ class ZcashWalletApp : DaggerApplication() {
override fun uncaughtException(t: Thread?, e: Throwable?) {
// trackCrash(e, "Top-level exception wasn't caught by anything else!")
// Analytics.clear()
twig("Uncaught Exception: $e")
ogHandler.uncaughtException(t, e)
}
}

View File

@ -5,7 +5,7 @@ import cash.z.ecc.android.ui.MainActivityModule
import cash.z.ecc.android.ui.detail.WalletDetailFragmentModule
import cash.z.ecc.android.ui.home.HomeFragmentModule
import cash.z.ecc.android.ui.receive.ReceiveFragmentModule
import cash.z.ecc.android.ui.send.SendFragmentModule
import cash.z.ecc.android.ui.send.*
import cash.z.ecc.android.ui.setup.BackupFragmentModule
import cash.z.ecc.android.ui.setup.LandingFragmentModule
import dagger.BindsInstance
@ -27,7 +27,10 @@ import javax.inject.Singleton
// Fragments
HomeFragmentModule::class,
ReceiveFragmentModule::class,
SendFragmentModule::class,
SendAddressFragmentModule::class,
SendMemoFragmentModule::class,
SendConfirmFragmentModule::class,
SendFinalFragmentModule::class,
WalletDetailFragmentModule::class,
LandingFragmentModule::class,
BackupFragmentModule::class

View File

@ -3,6 +3,8 @@ package cash.z.ecc.android.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.ui.home.HomeViewModel
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import dagger.Binds
import dagger.Module
@ -21,6 +23,16 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(WalletSetupViewModel::class)
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(HomeViewModel::class)
abstract fun bindHomeViewModel(implementation: HomeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(SendViewModel::class)
abstract fun bindSendViewModel(implementation: SendViewModel): ViewModel
}
@Singleton

View File

@ -14,6 +14,10 @@ fun View.invisibleIf(isInvisible: Boolean) {
visibility = if (isInvisible) INVISIBLE else VISIBLE
}
fun View.disabledIf(isDisabled: Boolean) {
isEnabled = !isDisabled
}
fun View.onClickNavTo(navResId: Int) {
setOnClickListener {
(context as? MainActivity)?.navController?.navigate(navResId)
@ -32,6 +36,16 @@ fun View.onClickNavUp() {
}
}
fun View.onClickNavBack() {
setOnClickListener {
(context as? MainActivity)?.navController?.popBackStack()
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}
fun View.clicks() = channelFlow<View> {
setOnClickListener {
offer(this@clicks)

View File

@ -2,15 +2,25 @@ package cash.z.ecc.android.feedback
import cash.z.ecc.android.ZcashWalletApp
enum class NonUserAction(override val key: String, val description: String) : Feedback.Action {
FEEDBACK_STARTED("action.feedback.start", "feedback started"),
FEEDBACK_STOPPED("action.feedback.stop", "feedback stopped");
object Report {
enum class NonUserAction(override val key: String, val description: String) : Feedback.Action {
FEEDBACK_STARTED("action.feedback.start", "feedback started"),
FEEDBACK_STOPPED("action.feedback.stop", "feedback stopped"),
SYNC_START("action.feedback.synchronizer.start", "sync started");
override fun toString(): String = description
}
override fun toString(): String = description
}
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
SEED_CREATION("metric.seed.creation", "seed created")
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
ENTROPY_CREATED("metric.entropy.created", "entropy created"),
SEED_CREATED("metric.seed.created", "seed created"),
SEED_IMPORTED("metric.seed.imported", "seed imported"),
SEED_PHRASE_CREATED("metric.seedphrase.created", "seed phrase created"),
SEED_PHRASE_LOADED("metric.seedphrase.loaded", "seed phrase loaded"),
WALLET_CREATED("metric.wallet.created", "wallet created"),
WALLET_IMPORTED("metric.wallet.imported", "wallet imported"),
ACCOUNT_CREATED("metric.account.created", "account created")
}
}
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
@ -27,5 +37,5 @@ class LaunchMetric private constructor(private val metric: Feedback.TimeMetric)
override fun toString(): String = metric.toString()
}
fun <T> Feedback.measure(type: MetricType, block: () -> T) =
inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T =
this.measure(type.key, type.description, block)

View File

@ -13,7 +13,9 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
@ -21,6 +23,12 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.snackbar.Snackbar
import dagger.Module
import dagger.Provides
@ -33,18 +41,29 @@ import javax.inject.Inject
class MainActivity : DaggerAppCompatActivity() {
private var syncInit: (() -> Unit)? = null
@Inject
lateinit var feedback: Feedback
@Inject
lateinit var feedbackCoordinator: FeedbackCoordinator
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val sendViewModel: SendViewModel by viewModels { viewModelFactory }
lateinit var navController: NavController
private val mediaPlayer: MediaPlayer = MediaPlayer()
private var snackbar: Snackbar? = null
lateinit var synchronizer: Synchronizer
val clipboard get() = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
@ -78,7 +97,7 @@ class MainActivity : DaggerAppCompatActivity() {
override fun onDestroy() {
lifecycleScope.launch {
feedback.report(NonUserAction.FEEDBACK_STOPPED)
feedback.report(FEEDBACK_STOPPED)
feedback.stop()
}
super.onDestroy()
@ -106,6 +125,29 @@ class MainActivity : DaggerAppCompatActivity() {
}
}
fun initSync() {
twig("Initializing synchronizer")
if (!::synchronizer.isInitialized) {
twig("Synchronizer didn't exist yet (this means we're opening an existing wallet). Creating it now.")
val initializer = Initializer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443).also { it.open() }
synchronizer = Synchronizer(ZcashWalletApp.instance, initializer)
}
feedback.report(SYNC_START)
synchronizer.start(lifecycleScope)
if (syncInit != null) {
syncInit!!()
syncInit = null
}
}
fun initializeAccount(seed: ByteArray, birthday: Initializer.WalletBirthday? = null) {
twig("Initializing accounts")
feedback.measure(Report.MetricType.ACCOUNT_CREATED) {
synchronizer =
Synchronizer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443, seed, birthday)
}
}
fun playSound(fileName: String) {
mediaPlayer.apply {
if (isPlaying) stop()
@ -131,18 +173,15 @@ class MainActivity : DaggerAppCompatActivity() {
}
fun copyAddress(view: View) {
// TODO: get address from synchronizer
val address =
"zs1qduvdyuv83pyygjvc4cfcuc2wj5flnqn730iigf0tjct8k5ccs9y30p96j2gvn9gzyxm6q0vj12c4"
val clipboard: ClipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(
ClipData.newPlainText(
"Z-Address",
address
lifecycleScope.launch {
clipboard.setPrimaryClip(
ClipData.newPlainText(
"Z-Address",
synchronizer.getAddress()
)
)
)
showMessage("Address copied!", "Sweet")
showMessage("Address copied!", "Sweet")
}
}
private fun showMessage(message: String, action: String) {
@ -174,6 +213,15 @@ class MainActivity : DaggerAppCompatActivity() {
if (!it.isShownOrQueued) it.show()
}
}
// TODO: refactor initialization and remove the need for this
fun onSyncInit(initBlock: () -> Unit) {
if (::synchronizer.isInitialized) {
initBlock()
} else {
syncInit = initBlock
}
}
}
@Module

View File

@ -5,15 +5,23 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.ui.MainActivity
import dagger.android.support.DaggerFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlin.coroutines.coroutineContext
abstract class BaseFragment<T : ViewBinding> : DaggerFragment() {
val mainActivity: MainActivity? get() = activity as MainActivity?
lateinit var binding: T
lateinit var resumedScope: CoroutineScope
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -23,6 +31,18 @@ abstract class BaseFragment<T : ViewBinding> : DaggerFragment() {
return binding.root
}
override fun onResume() {
super.onResume()
resumedScope = lifecycleScope.coroutineContext.let {
CoroutineScope(it + SupervisorJob(it[Job]))
}
}
override fun onPause() {
super.onPause()
resumedScope.cancel()
}
// inflate is static in the ViewBinding class so we can't handle this ourselves
// each fragment must call FragmentMyLayoutBinding.inflate(inflater)
abstract fun inflate(@NonNull inflater: LayoutInflater): T

View File

@ -40,6 +40,8 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
mainActivity?.showSnackbar("Feedback not yet implemented.")
}
}
private fun onViewLogs() {
loadLogFileAsText().let { logText ->
if (logText == null) {

View File

@ -1,89 +1,255 @@
package cash.z.ecc.android.ui.home
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.di.annotation.FragmentScope
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.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCING
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
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private lateinit var numberPad: List<TextView>
private lateinit var uiModel: HomeViewModel.UiModel
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
private val viewModel: HomeViewModel by activityViewModels { viewModelFactory }
private val _typedChars = ConflatedBroadcastChannel<Char>()
private val typedChars = _typedChars.asFlow()
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
FragmentHomeBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
numberPad = arrayListOf(
buttonNumberPad0,
buttonNumberPad1,
buttonNumberPad2,
buttonNumberPad3,
buttonNumberPad4,
buttonNumberPad5,
buttonNumberPad6,
buttonNumberPad7,
buttonNumberPad8,
buttonNumberPad9,
buttonNumberPadDecimal,
buttonNumberPadBack
)
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive)
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
hitAreaScan.onClickNavTo(R.id.action_nav_home_to_nav_send)
textBannerAction.setOnClickListener {
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
}
}
// TODO: trigger this from presenter
onNoFunds()
}
//
// LifeCycle
//
override fun onAttach(context: Context) {
twig("HomeFragment.onAttach")
super.onAttach(context)
// call initSync either now or later (after initializing DBs with newly created seed)
walletSetup.checkSeed().onEach {
twig("Checking seed")
when(it) {
NO_SEED -> {
twig("Seed not found, therefore, launching seed creation flow")
// interact with user to create, backup and verify seed
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
// leads to a call to initSync(), later (after accounts are created from seed)
}
else -> {
twig("Found seed. Re-opening existing wallet")
mainActivity?.initSync()
}
}
}.launchIn(lifecycleScope)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twig("HomeFragment.onViewCreated uiModel: ${::uiModel.isInitialized} saved: ${savedInstanceState != null}")
with(binding) {
numberPad = arrayListOf(
buttonNumberPad0.asKey(),
buttonNumberPad1.asKey(),
buttonNumberPad2.asKey(),
buttonNumberPad3.asKey(),
buttonNumberPad4.asKey(),
buttonNumberPad5.asKey(),
buttonNumberPad6.asKey(),
buttonNumberPad7.asKey(),
buttonNumberPad8.asKey(),
buttonNumberPad9.asKey(),
buttonNumberPadDecimal.asKey(),
buttonNumberPadBack.asKey()
)
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive)
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
// hitAreaScan.onClickNavTo(R.id.action_nav_home_to_nav_send)
textBannerAction.setOnClickListener {
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
}
buttonSend.setOnClickListener {
onSend()
}
}
if (::uiModel.isInitialized) {
twig("uiModel exists!")
onModelUpdated(HomeViewModel.UiModel(), uiModel)
} else {
twig("uiModel does not exist!")
mainActivity?.onSyncInit {
viewModel.initialize(mainActivity!!.synchronizer, typedChars)
}
}
}
override fun onResume() {
super.onResume()
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
viewModel.uiModels.scanReduce { old, new ->
onModelUpdated(old, new)
new
}.catch { e ->
twig("exception while processing uiModels $e")
}.launchIn(resumedScope)
// 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 {
(mainActivity!!.synchronizer as SdkSynchronizer).refreshBalance()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
twig("HomeFragment.onSaveInstanceState")
if (::uiModel.isInitialized) {
outState.putParcelable("uiModel", uiModel)
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
savedInstanceState?.let { inState ->
twig("HomeFragment.onViewStateRestored")
onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
}
}
//
// Public UI API
//
fun setSendEnabled(enabled: Boolean) {
binding.buttonSend.apply {
isEnabled = enabled
backgroundTintList = ColorStateList.valueOf( resources.getColor( if(enabled) R.color.colorPrimary else R.color.zcashWhite_24) )
}
}
fun setProgress(progress: Int) {
progress.let {
if (it < 100) {
setBanner("Downloading . . . $it%", NONE)
} else {
setBanner("Scanning . . .", NONE)
}
}
}
fun setSendAmount(amount: String) {
binding.textSendAmount.text = "\$$amount"
mainActivity?.sendViewModel?.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
binding.buttonSend.disabledIf(amount == "0")
}
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)) {
"(expecting +${(totalBalance - availableBalance).convertZatoshiToZecString()} ZEC in change)"
} else {
"(enter an amount to send)"
}
}
}
fun setSendText(buttonText: String = "Send Amount") {
binding.buttonSend.text = buttonText
}
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
//
private fun onModelUpdated(old: HomeViewModel.UiModel, new: HomeViewModel.UiModel) {
twig(new.toString())
uiModel = new
if (old.pendingSend != new.pendingSend) {
setSendAmount(new.pendingSend)
}
// TODO: handle stopped and disconnected flows
if (new.status == SYNCING) onSyncing(new) else onSynced(new)
setSendEnabled(new.isSendEnabled)
}
private fun onSyncing(uiModel: HomeViewModel.UiModel) {
setProgress(uiModel.progress) // calls setBanner
setAvailable()
setSendText("Syncing Blockchain…")
}
private fun onSynced(uiModel: HomeViewModel.UiModel) {
if (!uiModel.hasFunds) {
onNoFunds()
} else {
setBanner("")
setAvailable(uiModel.availableBalance, uiModel.totalBalance)
setSendText()
}
}
private fun onSend() {
mainActivity?.navController?.navigate(R.id.action_nav_home_to_send)
}
private fun onBannerAction(action: BannerAction) {
when (action) {
LEARN_MORE -> {
FUND_NOW -> {
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")
@ -106,25 +272,19 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
private fun onNoFunds() {
setBanner("No Balance", LEARN_MORE)
setBanner("No Balance", FUND_NOW)
}
private 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
}
}
//
// Inner classes and extensions
//
enum class BannerAction(val action: String) {
LEARN_MORE("Learn More"),
FUND_NOW("Fund Now"),
CANCEL("Cancel"),
CLEAR("");
NONE(""),
CLEAR("clear");
companion object {
fun from(action: String?): BannerAction {
@ -135,6 +295,54 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
}
}
private fun TextView.asKey(): TextView {
val c = text[0]
setOnClickListener {
lifecycleScope.launch {
twig("CHAR TYPED: $c")
_typedChars.send(c)
}
}
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")
}
}

View File

@ -0,0 +1,72 @@
package cash.z.ecc.android.ui.home
import android.os.Parcelable
import androidx.lifecycle.ViewModel
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import cash.z.wallet.sdk.ext.twig
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.flow.*
import javax.inject.Inject
class HomeViewModel @Inject constructor() : ViewModel() {
lateinit var uiModels: Flow<UiModel>
fun initialize(
synchronizer: Synchronizer,
typedChars: Flow<Char>
) {
twig("init called")
val zec = typedChars.scan("0") { acc, c ->
when {
// no-op cases
acc == "0" && c == '0'
|| (c == '<' && acc == "0")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c $typedChars ")
acc
}
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
"0"
}
c == '<' -> {twig("triggered: 3")
acc.substring(0, acc.length - 1)
}
acc == "0" && c != '.' -> {twig("triggered: 4 $typedChars")
c.toString()
}
else -> {twig("triggered: 5 $typedChars")
"$acc$c"
}
}
}
uiModels = synchronizer.run {
combine(status, progress, balances, zec) { s, p, b, z->
UiModel(s, p, b.available, b.total, z)
}
}.conflate()
}
override fun onCleared() {
super.onCleared()
twig("HomeViewModel cleared!")
}
@Parcelize
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
val status: Synchronizer.Status = DISCONNECTED,
val progress: Int = 0,
val availableBalance: Long = -1L,
val totalBalance: Long = -1L,
val pendingSend: String = "0"
): Parcelable {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds: Boolean get() = availableBalance > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.0001
val isSynced: Boolean get() = status == SYNCED
val isSendEnabled: Boolean get() = isSynced && hasFunds
}
}

View File

@ -16,10 +16,14 @@ import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.fragment_receive.*
import kotlinx.coroutines.launch
import kotlin.math.floor
import kotlin.math.round
import kotlin.math.roundToInt
class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentReceiveBinding =
@ -51,23 +55,39 @@ class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
override fun onResume() {
super.onResume()
lifecycleScope.launch {
onAddressLoaded("zs1qduvdyuv83pyygjvc4cfcuc2wj5flnqn730iigf0tjct8k5ccs9y30p96j2gvn9gzyxm6q0vj12c4")
resumedScope.launch {
mainActivity?.synchronizer?.getAddress()?.let { address ->
onAddressLoaded(address)
}
}
}
private fun onAddressLoaded(address: String) {
Log.e("TWIG", "onAddressLoaded: $address length: ${address.length}")
twig("address loaded: $address length: ${address.length}")
qrecycler.load(address)
.withQuietZoneSize(3)
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)
.into(receive_qr_code)
address.chunked(address.length/8).forEachIndexed { i, part ->
address.distribute(8) { i, part ->
setAddressPart(i, part)
}
}
private fun <T> String.distribute(chunks: Int, block: (Int, String) -> T) {
val charsPerChunk = length / 8.0
val wholeCharsPerChunk = charsPerChunk.toInt()
val chunksWithExtra = ((charsPerChunk - wholeCharsPerChunk) * chunks).roundToInt()
repeat(chunks) { i ->
val part = if (i < chunksWithExtra) {
substring(i * (wholeCharsPerChunk + 1), (i + 1) * (wholeCharsPerChunk + 1))
} else {
substring(i * wholeCharsPerChunk + chunksWithExtra, (i + 1) * wholeCharsPerChunk + chunksWithExtra)
}
block(i, part)
}
}
private fun setAddressPart(index: Int, addressPart: String) {
Log.e("TWIG", "setting address for part $index) $addressPart")
val thinSpace = "\u2005" // 0.25 em space

View File

@ -0,0 +1,113 @@
package cash.z.ecc.android.ui.send
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendAddressBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
ClipboardManager.OnPrimaryClipChangedListener {
override fun inflate(inflater: LayoutInflater): FragmentSendAddressBinding =
FragmentSendAddressBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonNext.setOnClickListener {
onAddAddress()
}
binding.backButtonHitArea.onClickNavBack()
binding.textBannerAction.setOnClickListener {
onPaste()
}
binding.textBannerMessage.setOnClickListener {
onPaste()
}
binding.textAmount.text = "Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC"
binding.inputZcashAddress.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onAddAddress()
true
} else {
false
}
}
}
private fun onAddAddress() {
mainActivity?.sendViewModel?.toAddress = binding.inputZcashAddress.text.toString()
mainActivity?.navController?.navigate(R.id.action_nav_send_address_to_send_memo)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
}
override fun onDetach() {
super.onDetach()
mainActivity?.clipboard?.removePrimaryClipChangedListener(this)
}
override fun onResume() {
super.onResume()
updateClipboardBanner()
}
override fun onPrimaryClipChanged() {
twig("clipboard changed!")
updateClipboardBanner()
}
private fun updateClipboardBanner() {
binding.groupBanner.goneIf(loadAddressFromClipboard() == null)
}
private fun onPaste() {
mainActivity?.clipboard?.let { clipboard ->
if (clipboard.hasPrimaryClip()) {
binding.inputZcashAddress.setText(clipboard.text())
}
}
}
private fun loadAddressFromClipboard(): String? {
mainActivity?.clipboard?.apply {
if (hasPrimaryClip()) {
text()?.let { text ->
if (text.startsWith("zs") && text.length > 70) {
return@loadAddressFromClipboard text.toString()
}
// treat t-addrs differently in the future
if (text.startsWith("t1") && text.length > 32) {
return@loadAddressFromClipboard text.toString()
}
}
}
}
return null
}
private fun ClipboardManager.text(): CharSequence =
primaryClip!!.getItemAt(0).coerceToText(mainActivity)
}
@Module
abstract class SendAddressFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendAddressFragment
}

View File

@ -0,0 +1,53 @@
package cash.z.ecc.android.ui.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentSendConfirmBinding =
FragmentSendConfirmBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainActivity?.apply {
binding.buttonNext.setOnClickListener {
onSend()
}
binding.backButtonHitArea.onClickNavBack()
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.abbreviatedAddress()}?"
}
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
}
}
private fun onSend() {
mainActivity?.navController?.navigate(R.id.action_nav_send_confirm_to_send_final)
}
}
@Module
abstract class SendConfirmFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendConfirmFragment
}

View File

@ -0,0 +1,103 @@
package cash.z.ecc.android.ui.send
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.entity.*
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.math.min
import kotlin.random.Random
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
FragmentSendFinalBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonNext.setOnClickListener {
onExit()
}
binding.backButtonHitArea.setOnClickListener {
onExit()
}
binding.textConfirmation.text =
"Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${mainActivity?.sendViewModel?.toAddress?.abbreviatedAddress()}"
mainActivity?.sendViewModel?.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.apply {
sendViewModel.send(synchronizer).onEach {
onPendingTxUpdated(it)
}.launchIn(mainActivity?.lifecycleScope!!)
}
}
override fun onResume() {
super.onResume()
flow {
val max = binding.progressHorizontal.max - 1
var progress = 0
while (progress < max) {
emit(progress)
delay(Random.nextLong(1000))
progress++
}
}.onEach {
binding.progressHorizontal.progress = it
}.launchIn(resumedScope)
}
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
val id = pendingTransaction?.id ?: -1
var isSending = true
val message = when {
pendingTransaction == null -> "Transaction not found"
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation . . ."
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
pendingTransaction.isCreating() -> "Creating transaction . . ."
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
}
twig("Pending TX Updated: $message")
binding.textStatus.apply {
text = "$text\n$message"
}
binding.backButton.goneIf(!binding.textStatus.text.toString().contains("Awaiting"))
binding.buttonNext.goneIf(isSending)
binding.progressHorizontal.goneIf(!isSending)
}
private fun onExit() {
mainActivity?.navController?.popBackStack(R.id.send_navigation, true)
}
}
@Module
abstract class SendFinalFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendFinalFragment
}

View File

@ -1,29 +0,0 @@
package cash.z.ecc.android.ui.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import cash.z.ecc.android.databinding.FragmentSendBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ui.base.BaseFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
class SendFragment : BaseFragment<FragmentSendBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.backButtonHitArea.onClickNavUp()
}
}
@Module
abstract class SendFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendFragment
}

View File

@ -0,0 +1,61 @@
package cash.z.ecc.android.ui.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ui.base.BaseFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentSendMemoBinding =
FragmentSendMemoBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonNext.setOnClickListener {
onAddMemo()
}
binding.buttonSkip.setOnClickListener {
binding.inputMemo.setText("")
mainActivity?.sendViewModel?.memo = ""
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
}
binding.backButtonHitArea.onClickNavBack()
binding.radioIncludeAddress.setOnClickListener {
if (binding.radioIncludeAddress.isActivated) {
binding.radioIncludeAddress.isChecked = false
} else {
binding.radioIncludeAddress.isActivated = true
}
}
binding.inputMemo.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onAddMemo()
true
} else {
false
}
}
binding.radioIncludeAddress.requestFocus()
}
private fun onAddMemo() {
mainActivity?.sendViewModel?.memo = binding.inputMemo.text.toString()
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
}
}
@Module
abstract class SendMemoFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendMemoFragment
}

View File

@ -0,0 +1,32 @@
package cash.z.ecc.android.ui.send
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.entity.PendingTransaction
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class SendViewModel @Inject constructor(var lockBox: LockBox) : ViewModel() {
fun send(synchronizer: Synchronizer): Flow<PendingTransaction> {
val keys = (synchronizer as SdkSynchronizer).rustBackend!!.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
)
return synchronizer.sendToAddress(
keys[0],
zatoshiAmount,
toAddress,
memo
).onEach {
twig(it.toString())
}
}
var toAddress: String = ""
var memo: String = ""
var zatoshiAmount: Long = -1L
}

View File

@ -15,18 +15,21 @@ 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.annotation.FragmentScope
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
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 dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
@ -79,7 +82,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
}
private fun applySpan(vararg textViews: TextView) {
private fun applySpan(vararg textViews: TextView) = lifecycleScope.launch {
val words = loadSeedWords()
val thinSpace = "\u2005" // 0.25 em space
textViews.forEachIndexed { index, textView ->
@ -92,11 +95,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
}
}
private fun loadSeedWords(): List<CharArray> {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seed = lockBox.getBytes(LockBoxKey.SEED)!!
return mnemonics.nextMnemonicList(seed)
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(LockBoxKey.SEED_PHRASE)!!
val result = mnemonics.toWordList(seedPhrase)
result
}
}
}

View File

@ -12,19 +12,17 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentLandingBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.feedback.MetricType.SEED_CREATION
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.isEmulator
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.wallet.sdk.Initializer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
@ -46,6 +44,24 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
"backup" -> onBackupWallet()
}
}
binding.buttonNegative.setOnLongClickListener {
if (binding.buttonNegative.text.toString().toLowerCase() == "restore") {
MaterialAlertDialogBuilder(activity)
.setMessage("Would you like to import the dev wallet?\n\nIf so, please only send 0.0001 ZEC at a time and return some later so that the account remains funded.")
.setTitle("Import Dev Wallet?")
.setCancelable(true)
.setPositiveButton("Import") { dialog, _ ->
dialog.dismiss()
onUseDevWallet()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
true
}
false
}
binding.buttonNegative.setOnClickListener {
when (binding.buttonNegative.text.toString().toLowerCase()) {
"restore" -> onRestoreWallet()
@ -84,23 +100,50 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
private fun onRestoreWallet() {
if (ZcashWalletApp.instance.isEmulator()) {
onEnterWallet()
} else {
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
}
private fun onUseDevWallet() {
val seedPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val birthday = 663174//626599
mainActivity?.apply {
lifecycleScope.launch {
initializeAccount(
walletSetup.importWallet(feedback, seedPhrase.toCharArray()),
Initializer.loadBirthdayFromAssets(ZcashWalletApp.instance, birthday)
)
initSync()
}
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet imported! Congratulations!"
binding.buttonNegative.text = "Skip"
binding.buttonPositive.text = "Backup"
playSound("sound_receive_small.mp3")
vibrateSuccess()
}
}
// TODO: move this to the ViewModel but doing so requires fixing dagger so do that as a separate PR
private fun onNewWallet() {
mainActivity?.feedback?.measure(SEED_CREATION) {
walletSetup.createSeed()
}
lifecycleScope.launch {
val ogText = binding.buttonPositive.text
binding.buttonPositive.text = "creating"
binding.buttonPositive.isEnabled = false
binding.textMessage.text = "Wallet created! Congratulations!"
binding.buttonNegative.text = "Skip"
binding.buttonPositive.text = "Backup"
mainActivity?.playSound("sound_receive_small.mp3")
mainActivity?.vibrateSuccess()
mainActivity?.apply {
initializeAccount(walletSetup.createWallet(feedback))
initSync()
}
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet created! Congratulations!"
binding.buttonNegative.text = "Skip"
binding.buttonPositive.text = "Backup"
mainActivity?.playSound("sound_receive_small.mp3")
mainActivity?.vibrateSuccess()
}
}
private fun onBackupWallet() {

View File

@ -1,14 +1,16 @@
package cash.z.ecc.android.ui.setup
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.feedback.Feedback
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.LockBoxKey.HAS_BACKUP
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_SEED
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.SEED
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val lockBox: LockBox) :
@ -20,27 +22,80 @@ class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val loc
fun checkSeed(): Flow<WalletSetupState> = flow {
when {
lockBox.getBoolean(HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
lockBox.getBoolean(LockBoxKey.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(LockBoxKey.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
else -> emit(NO_SEED)
}
}
fun createSeed() {
check(!lockBox.getBoolean(HAS_SEED)) {
/**
* Take all the steps necessary to create a new wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
suspend fun createWallet(feedback: Feedback): 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!"
}
mnemonics.apply {
lockBox.setBytes(SEED, nextSeed())
lockBox.setBoolean(HAS_SEED, true)
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_CREATED) { toSeed(seedPhrase) }.let { bip39Seed ->
lockBox.setCharsUtf8(LockBoxKey.SEED_PHRASE, seedPhrase)
lockBox.setBoolean(LockBoxKey.HAS_SEED_PHRASE, true)
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
lockBox.setBoolean(LockBoxKey.HAS_SEED, true)
bip39Seed
}
}
}
}
}
}
/**
* Take all the steps necessary to import a wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
suspend fun importWallet(
feedback: Feedback,
seedPhrase: CharArray
): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot import 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!"
}
feedback.measure(WALLET_IMPORTED) {
mnemonics.run {
feedback.measure(SEED_IMPORTED) { toSeed(seedPhrase) }.let { bip39Seed ->
lockBox.setCharsUtf8(LockBoxKey.SEED_PHRASE, seedPhrase)
lockBox.setBoolean(LockBoxKey.HAS_SEED_PHRASE, true)
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
lockBox.setBoolean(LockBoxKey.HAS_SEED, true)
bip39Seed
}
}
}
}
object LockBoxKey {
const val SEED = "cash.z.ecc.android.SEED1"
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED1"
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP1"
const val SEED = "cash.z.ecc.android.SEED"
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:endColor="@color/colorPrimaryDark"
android:startColor="@color/colorPrimary"
android:type="radial"
android:centerY="0.36"
android:centerX="0.50"
android:gradientRadius="640dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
@ -39,7 +40,7 @@
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="drum"
tools:text="drum"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -51,7 +52,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7" />
@ -61,7 +62,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_10" />
@ -71,7 +72,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_7"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_13" />
@ -81,7 +82,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_10"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_16" />
@ -91,7 +92,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_13"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_19" />
@ -101,7 +102,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_16"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_22" />
@ -111,7 +112,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_19" />
@ -126,7 +127,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="fitness"
tools:text="fitness"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
@ -137,7 +138,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
@ -148,7 +149,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_11"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
@ -159,7 +160,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_14"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_8" />
@ -170,7 +171,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_17"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_11" />
@ -181,7 +182,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_20"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_14" />
@ -192,7 +193,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_23"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_17" />
@ -203,7 +204,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_20" />
@ -218,7 +219,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="goals"
tools:text="goals"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
@ -229,7 +230,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_9"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
@ -240,7 +241,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_12"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
@ -251,7 +252,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_15"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_9" />
@ -262,7 +263,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_18"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_12" />
@ -273,7 +274,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_21"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_15" />
@ -284,7 +285,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_24"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_18" />
@ -295,7 +296,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
style="@style/Zcash.TextAppearance.AddressPart"
android:text="word"
tools:text="word"
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
app:layout_constraintTop_toBottomOf="@id/text_address_part_21" />
<!--

View File

@ -13,28 +13,28 @@
<!-- TODO: redo these keylines to match the designs, exactly -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guieline_bottom_buttons"
android:id="@+id/guideline_bottom_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7017784" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guieline_keyline_start"
android:id="@+id/guideline_keyline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.054" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guieline_keyline_end"
android:id="@+id/guideline_keyline_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.946" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guieline_keyline_bottom"
android:id="@+id/guideline_keyline_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
@ -73,7 +73,7 @@
android:layout_height="wrap_content"
android:text="Backup\nWallet"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintTop_toTopOf="@id/back_button" />
<View
@ -81,10 +81,10 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_banner_large"
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/guieline_bottom_buttons"
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"/>
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/guideline_bottom_buttons"
app:layout_constraintBottom_toBottomOf="@id/guideline_keyline_bottom"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_feedback"
@ -97,8 +97,8 @@
android:layout_marginStart="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintVertical_bias="0.8"/>
<com.google.android.material.button.MaterialButton
@ -109,7 +109,7 @@
android:text="View Logs"
android:textColor="@color/text_light"
app:layout_constraintTop_toBottomOf="@id/button_feedback"
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"
app:layout_constraintBottom_toBottomOf="@id/guideline_keyline_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.2"/>

View File

@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<View
@ -27,14 +28,28 @@
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.04" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.13" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_send_amount_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.23" />
<TextView
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12.34581242 ZEC"
android:text="Updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:visibility="gone"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@id/label_balance"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
@ -53,6 +68,17 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_balance_available" />
<TextView
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(enter an amount to send)"
android:visibility="gone"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_balance_available" />
<!-- -->
<!-- Number Pad -->
@ -213,20 +239,23 @@
app:layout_constraintTop_toBottomOf="@id/button_number_pad_9"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
<!-- TODO: properly style this as a button with ripples -->
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/button_send"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/background_button_rounded"
android:gravity="center"
style="@style/Zcash.Button"
android:text="Send Amount"
android:enabled="false"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
android:gravity="center"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys" />
app:layout_constraintTop_toBottomOf="@id/guide_keys"/>
<View
android:id="@+id/layer_lock"
@ -234,6 +263,7 @@
android:layout_height="match_parent"
android:clickable="true"
android:background="#D0000000"
tools:visibility="gone"
android:elevation="5dp" />
<!-- -->
@ -322,36 +352,25 @@
app:layout_constraintTop_toBottomOf="@id/button_send"
app:layout_constraintVertical_bias="@dimen/ratio_golden_small" />
<ImageView
android:id="@+id/icon_zec_symbol"
android:elevation="6dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:tint="@color/colorAccent"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/text_send_amount"
app:layout_constraintEnd_toStartOf="@id/text_send_amount"
app:layout_constraintHeight_percent="0.052"
app:layout_constraintTop_toTopOf="@id/text_send_amount"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintWidth_percent="0.060"
app:srcCompat="@drawable/ic_zec_symbol" />
<TextView
android:id="@+id/text_send_amount"
android:elevation="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="$0"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="72dp"
app:layout_constraintBottom_toTopOf="@id/guide_keys"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@id/guideline_send_amount_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@id/guideline_send_amount_top" />
<!-- -->
<!-- Banner -->
@ -379,7 +398,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Learn More"
android:text="Fund Now"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
@ -395,6 +414,7 @@
android:id="@+id/group_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="text_banner_message, text_banner_action" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -49,7 +49,7 @@
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Welcome to the Birch Wallet!"
android:text="Welcome to the zECC Wallet!"
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/text_light"
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintHorizontal_bias="0.05"/>
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_light"
android:text="SCAN"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home"
xmlns:tools="http://schemas.android.com/tools">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/text_light"
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintHorizontal_bias="0.05"/>
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
tools:text="Sending 12.34121212 ZEC"
android:textColor="@color/text_light"
android:autoSizeTextType="uniform"
android:maxLines="1"
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/back_button"
app:layout_constraintBottom_toBottomOf="@id/back_button" />
<!-- Input: Address -->
<EditText
android:id="@+id/input_zcash_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:hint="@string/send_hint_input_zcash_address"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions"
android:paddingRight="76dp"
android:singleLine="true"
android:paddingTop="0dp"
android:textColor="@color/text_light"
app:backgroundTint="@color/colorPrimary"
android:textColorHint="@color/text_light_dimmed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.84"
app:layout_constraintVertical_bias="0.2"/>
<TextView
android:id="@+id/text_address_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:fontFamily="@font/inconsolata"
android:includeFontPadding="false"
android:textColor="@android:color/holo_red_light"
android:textSize="12dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/input_zcash_address"
app:layout_constraintTop_toBottomOf="@id/input_zcash_address"
tools:text="invalid address" />
<!-- Scan QR code -->
<ImageView
android:id="@+id/image_scan_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingTop="10dp"
android:paddingEnd="1dp"
android:paddingBottom="24dp"
android:tint="@color/zcashWhite"
app:layout_constraintBottom_toBottomOf="@id/input_zcash_address"
app:layout_constraintEnd_toEndOf="@id/input_zcash_address"
app:layout_constraintTop_toTopOf="@id/input_zcash_address"
app:srcCompat="@drawable/ic_qrcode_24dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Next"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
app:layout_constraintTop_toBottomOf="@+id/input_zcash_address" />
<!-- -->
<!-- Banner -->
<!-- -->
<TextView
android:id="@+id/text_banner_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_banner"
android:elevation="6dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:text="Address on clipboard!"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
app:layout_constraintStart_toStartOf="@+id/input_zcash_address"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"/>
<TextView
android:id="@+id/text_banner_action"
android:elevation="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Paste"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"
app:layout_constraintEnd_toEndOf="@id/text_banner_message" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="text_banner_message, text_banner_action"
android:visibility="visible"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_home">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_confirmation"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline4"
android:gravity="center"
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?"
android:textColor="@color/text_light"
android:maxLines="3"
android:autoSizeTextType="uniform"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.21"/>
<RadioButton
android:id="@+id/radio_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Includes memo"
android:enabled="false"
style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+.0001 Network Fee"
style="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/colorAccent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.84"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Tap\nto send ZEC"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.48" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_send_final"
android:animateLayoutChanges="true"
>
<View
android:id="@+id/guide_keys"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.3"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.38196601125"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.58"
app:layout_constraintWidth_percent="0.7475728155" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_close_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<TextView
android:id="@+id/text_confirmation"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline4"
android:gravity="center"
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?"
android:textColor="@color/text_dark"
android:maxLines="3"
android:autoSizeTextType="uniform"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.21"/>
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_dark"
tools:text="Creating transaction..."
android:textSize="16dp"
app:layout_constraintTop_toBottomOf="@id/radio_include_address"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.1"/>
<ProgressBar
android:id="@+id/progress_horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="@id/text_status"
app:layout_constraintEnd_toEndOf="@id/text_status"
app:layout_constraintTop_toBottomOf="@id/text_status"
android:indeterminate="false"
style="?android:attr/progressBarStyleHorizontal"
android:max="150"
android:foregroundTint="@color/zcashBlack_87" />
<RadioButton
android:id="@+id/radio_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="Includes memo"
android:enabled="false"
android:textColor="@color/text_dark_dimmed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
style="@style/Zcash.Button.OutlinedButton"
app:strokeColor="@color/text_dark"
android:padding="12dp"
android:text="Finished"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<EditText
android:id="@+id/input_memo"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_banner"
android:elevation="6dp"
android:gravity="top"
android:imeActionLabel="add memo"
android:inputType="textImeMultiLine"
android:imeOptions="actionDone"
android:hint="Add a memo here"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.185"
app:layout_constraintWidth_percent="0.8"
tools:text="This is my memo" />
<RadioButton
android:id="@+id/radio_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Include your sending address in memo"
app:layout_constraintStart_toStartOf="@+id/input_memo"
app:layout_constraintTop_toBottomOf="@+id/input_memo" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="Your transaction is shielded and your address is unavailable to recipient"
app:layout_constraintEnd_toEndOf="@id/input_memo"
app:layout_constraintStart_toStartOf="@id/input_memo"
app:layout_constraintTop_toBottomOf="@id/radio_include_address" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:padding="12dp"
android:text="Add Memo"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8"
app:layout_constraintWidth_percent="0.68" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_skip"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:padding="12dp"
android:text="Send without memo"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/button_next"
app:layout_constraintStart_toStartOf="@id/button_next"
app:layout_constraintTop_toBottomOf="@id/button_next" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -13,22 +13,17 @@
<action
android:id="@+id/action_nav_home_to_nav_receive"
app:destination="@id/nav_receive" />
<action
android:id="@+id/action_nav_home_to_nav_send"
app:destination="@id/nav_send" />
<action
android:id="@+id/action_nav_home_to_nav_detail"
app:destination="@id/nav_detail" />
<action
android:id="@+id/action_nav_home_to_create_wallet"
app:destination="@id/wallet_setup_navigation" />
<action
android:id="@+id/action_nav_home_to_send"
app:destination="@id/send_navigation" />
</fragment>
<fragment
android:id="@+id/nav_send"
android:name="cash.z.ecc.android.ui.send.SendFragment"
tools:layout="@layout/fragment_send" />
<fragment
android:id="@+id/nav_receive"
android:name="cash.z.ecc.android.ui.receive.ReceiveFragment"
@ -44,5 +39,6 @@
</fragment>
<include app:graph="@navigation/wallet_setup_navigation" />
<include app:graph="@navigation/send_navigation" />
</navigation>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/send_navigation"
app:startDestination="@+id/nav_send_address">
<fragment
android:id="@+id/nav_send_address"
android:name="cash.z.ecc.android.ui.send.SendAddressFragment"
tools:layout="@layout/fragment_send_address" >
<action
android:id="@+id/action_nav_send_address_to_send_memo"
app:destination="@id/nav_send_memo"/>
</fragment>
<fragment
android:id="@+id/nav_send_memo"
android:name="cash.z.ecc.android.ui.send.SendMemoFragment"
tools:layout="@layout/fragment_send_memo" >
<action
android:id="@+id/action_nav_send_memo_to_send_confirm"
app:destination="@id/nav_send_confirm"/>
</fragment>
<fragment
android:id="@+id/nav_send_confirm"
android:name="cash.z.ecc.android.ui.send.SendConfirmFragment"
tools:layout="@layout/fragment_send_confirm" >
<action
android:id="@+id/action_nav_send_confirm_to_send_final"
app:destination="@id/nav_send_final"/>
</fragment>
<fragment
android:id="@+id/nav_send_final"
android:name="cash.z.ecc.android.ui.send.SendFinalFragment"
tools:layout="@layout/fragment_send_final" >
</fragment>
</navigation>

View File

@ -12,7 +12,9 @@
tools:layout="@layout/fragment_landing" >
<action
android:id="@+id/action_nav_landing_to_nav_backup"
app:destination="@id/nav_backup" />
app:destination="@id/nav_backup"
app:popUpTo="@id/nav_landing"
app:popUpToInclusive="true"/>
</fragment>
<fragment

View File

@ -24,6 +24,7 @@
<color name="zcashWhite">#F5F5F5</color>
<color name="zcashWhite_12">#1FFFFFFF</color>
<color name="zcashWhite_24">#3DFFFFFF</color>
<color name="zcashWhite_40">#66FFFFFF</color>
<color name="zcashWhite_50">#80FFFFFF</color>
<color name="zcashWhite_60">#A3FFFFFF</color>

View File

@ -4,4 +4,7 @@
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
</resources>

View File

@ -26,11 +26,15 @@
<item name="android:background">@drawable/selector_pressed_ripple_circle</item>
</style>
<style name="Zcash.Button.White" parent="Widget.MaterialComponents.Button">
<item name="backgroundTint">@android:color/white</item>
<style name="Zcash.Button" parent="Widget.MaterialComponents.Button">
<item name="backgroundTint">@color/colorPrimary</item>
<item name="android:textColor">@color/text_dark</item>
</style>
<style name="Zcash.Button.White">
<item name="backgroundTint">@android:color/white</item>
</style>
<style name="Zcash.Button.OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="strokeColor">@color/zcashWhite</item>
</style>
@ -65,7 +69,7 @@
<style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">cut</item>
<item name="cornerSize">8dp</item>
<item name="cornerSize">10dp</item>
</style>
<style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="cornerFamily">cut</item>

View File

@ -0,0 +1,54 @@
package cash.z.ecc.android
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.math.round
import kotlin.math.roundToInt
class ScratchPad {
val t get() = System.currentTimeMillis()
var t0 = 0L
val Δt get() = t - t0
@Test
fun testMarblesCombine() = runBlocking {
var started = false
val flow = flowOf(1, 2, 3, 4, 5, 6, 7, 8, 9).onEach {
delay(100)
if (!started) {
t0 = t
started = true
}
println("$Δt\temitting $it");
}
val flow2 = flowOf("a", "b", "c", "d", "e", "f").onEach { delay(150); println("$Δt\temitting $it")}
val flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it")}
combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart {
t0 = t
}.collect {
// if (!started) {
// println("$Δt until first emission")
// t0 = t
// started = true
// }
println("$Δt\t$it") // Will print "1a 2a 2b 2c"
}
}
@Test
fun testMarblesScan() = runBlocking {
val flow = flowOf(1, 2, 3, 4, 5)
flow.scanReduce { accumulator, value ->
println("was: $accumulator now: $value")
value
}.collect {
println("got $it")
}
}
}

View File

@ -92,12 +92,13 @@ class Feedback(capacity: Int = 256) {
* will run concurrently--meaning a "happens before" relationship between the measurer and the
* measured cannot be established and thereby the concurrent action cannot be timed.
*/
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T) {
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T): T {
ensureScope()
val metric = TimeMetric(key, description.toString()).markTime()
block()
val result = block()
metric.markTime()
report(metric)
return result
}
/**

View File

@ -19,6 +19,8 @@ class LockBox @Inject constructor(private val appContext: Context) : LockBoxProv
}
override fun setBytes(key: String, value: ByteArray) {
// using hex here because this library doesn't really work well for byte arrays
// but hopefully we can code to arrays and then change the underlying library, later
SecurePreferences.setValue(appContext, key, value.toHex())
}
@ -27,11 +29,13 @@ class LockBox @Inject constructor(private val appContext: Context) : LockBoxProv
}
override fun setCharsUtf8(key: String, value: CharArray) {
setBytes(key, value.toBytes())
// Using string here because this library doesn't work well for char arrays
// but hopefully we can code to arrays and then change the underlying library, later
SecurePreferences.setValue(appContext, key, String(value))
}
override fun getCharsUtf8(key: String): CharArray? {
return getBytes(key)?.fromBytes()
return SecurePreferences.getStringValue(appContext, key, null)?.toCharArray()
}

View File

@ -9,7 +9,7 @@ interface MnemonicProvider {
/**
* Generate a random seed.
*/
fun nextSeed(): ByteArray
fun nextEntropy(): ByteArray
/**
* Generate a random 24-word mnemonic phrase.

View File

@ -12,19 +12,19 @@ import javax.inject.Inject
// which expects a string so for that reason, we just use Strings here)
class Mnemonics @Inject constructor(): MnemonicProvider {
override fun nextSeed(): ByteArray {
override fun nextEntropy(): ByteArray {
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
SecureRandom().nextBytes(this)
}
}
override fun nextMnemonic(): CharArray {
return nextMnemonic(nextSeed())
return nextMnemonic(nextEntropy())
}
override fun nextMnemonic(seed: ByteArray): CharArray {
override fun nextMnemonic(entropy: ByteArray): CharArray {
return StringBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.toString().toCharArray()
@ -32,12 +32,12 @@ class Mnemonics @Inject constructor(): MnemonicProvider {
}
override fun nextMnemonicList(): List<CharArray> {
return nextMnemonicList(nextSeed())
return nextMnemonicList(nextEntropy())
}
override fun nextMnemonicList(seed: ByteArray): List<CharArray> {
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> {
return WordListBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.wordList
@ -46,7 +46,7 @@ class Mnemonics @Inject constructor(): MnemonicProvider {
override fun toSeed(mnemonic: CharArray): ByteArray {
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
return SeedCalculator().calculateSeed(mnemonic.toString(), "")
return SeedCalculator().calculateSeed(String(mnemonic), "")
}
override fun toWordList(mnemonic: CharArray): List<CharArray> {

View File

@ -1,2 +1,3 @@
rootProject.name='Zcash Wallet'
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox'
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':sdk'
project(":sdk").projectDir = file("../zcash-android-wallet-sdk")