Merge pull request #78 from zcash/release/sprint-2

Release/sprint-20-05
This commit is contained in:
Kevin Gorham 2020-02-12 08:06:20 -05:00 committed by GitHub
commit 4392f02dbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 5286 additions and 787 deletions

View File

@ -11,7 +11,7 @@ apply plugin: 'com.google.firebase.firebase-perf'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha10'
version = '1.0.0-alpha17'
android {
compileSdkVersion Deps.compileSdkVersion
@ -21,7 +21,7 @@ android {
applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_010
versionCode = 1_00_00_017
// 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'
@ -43,6 +43,17 @@ android {
matchingFallbacks = ['zcashmainnet', 'release']
}
}
// TODO: delete this test code
variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (names.contains("zcashtestnet") || names.contains("Zcashtestnet") || variant.buildType.name == "release") {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
buildTypes {
release {
minifyEnabled true
@ -102,6 +113,9 @@ dependencies {
implementation project(':lockbox')
implementation project(':sdk')
// Zcash
implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0'
// Kotlin
implementation Deps.Kotlin.STDLIB
@ -132,10 +146,10 @@ dependencies {
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"
implementation "io.grpc:grpc-protobuf-lite:1.21.0"
implementation "io.grpc:grpc-stub:1.21.0"
implementation "io.grpc:grpc-okhttp:1.27.0"
implementation "io.grpc:grpc-android:1.27.0"
implementation "io.grpc:grpc-protobuf-lite:1.27.0"
implementation "io.grpc:grpc-stub:1.27.0"
implementation 'javax.annotation:javax.annotation-api:1.3.2'
// solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)
// per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ
@ -143,22 +157,27 @@ dependencies {
// Analytics
implementation 'com.mixpanel.android:mixpanel-android:5.6.3'
implementation 'com.google.firebase:firebase-analytics:17.2.1'
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.crashlytics.sdk.android:crashlytics-ndk:2.1.1'
implementation 'com.google.firebase:firebase-perf:19.0.4'
implementation 'com.google.firebase:firebase-perf:19.0.5'
// QR Scanning
implementation 'com.google.firebase:firebase-ml-vision:24.0.1'
implementation 'androidx.camera:camera-core:1.0.0-alpha08'
implementation 'androidx.camera:camera-camera2:1.0.0-alpha08'
implementation "androidx.camera:camera-view:1.0.0-alpha05"
implementation "androidx.camera:camera-extensions:1.0.0-alpha05"
implementation "androidx.camera:camera-lifecycle:1.0.0-alpha02"
implementation 'androidx.camera:camera-core:1.0.0-alpha10'
implementation 'androidx.camera:camera-camera2:1.0.0-alpha10'
implementation "androidx.camera:camera-view:1.0.0-alpha07"
implementation "androidx.camera:camera-extensions:1.0.0-alpha07"
implementation "androidx.camera:camera-lifecycle:1.0.0-alpha10"
// Misc.
implementation 'com.airbnb.android:lottie:3.1.0'
implementation 'com.facebook.stetho:stetho:1.5.1'
// check for build errors at https://jitpack.io/com/github/gmale/chips-input-layout/<version>/build.log
// implementation 'com.github.gmale:chips-input-layout:4750760a7222bc04ca48266b387456d2b03541a7'
implementation project(':chipsinputlayout')
implementation 'androidx.annotation:annotation:1.1.0'
// Tests

View File

@ -10,7 +10,7 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/ZcashTheme">
<activity android:name=".ui.MainActivity" android:screenOrientation="portrait">

View File

@ -37,7 +37,7 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
override fun onCreate() {
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
Twig.plant(SilentTwig())
Twig.plant(TroubleshootingTwig())
creationTime = System.currentTimeMillis()
instance = this
// Setup handler for uncaught exceptions.
@ -72,7 +72,8 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
inner class ExceptionReporter(private val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(t: Thread?, e: Throwable?) {
twig("Uncaught Exception: $e caused by: ${e?.cause}")
coordinator.feedback.report(e)
// these are the only reported crashes that are considered fatal
coordinator.feedback.report(e, true)
coordinator.flush()
// can do this if necessary but first verify that we need it
runBlocking {

View File

@ -4,7 +4,6 @@ package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.di.viewmodel.ViewModelFactory
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
@ -26,6 +25,7 @@ abstract class ViewModelsActivityModule {
@ViewModelKey(WalletSetupViewModel::class)
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
/**
* Factory for view models that are created until before the Synchronizer exists. This is a
* little tricky because we cannot make them all in one place or else they won't be available

View File

@ -13,6 +13,13 @@ inline fun <reified VM : ViewModel> BaseFragment<*>.viewModel() = object : Lazy<
?: ViewModelProvider(this@viewModel, scopedFactory<VM>())[VM::class.java]
}
/**
* Create a view model that is scoped to the lifecycle of the activity.
*
* @param isSynchronizerScope true when this view model depends on the Synchronizer. False when this
* viewModel needs to be created before the synchronizer or otherwise has no dependency on it being
* available for use.
*/
inline fun <reified VM : ViewModel> BaseFragment<*>.activityViewModel(isSynchronizerScope: Boolean = true) = object : Lazy<VM> {
val cached: VM? = null
override fun isInitialized(): Boolean = cached != null

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.ext
import android.content.res.Resources
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.IntegerRes
@ -28,3 +29,9 @@ internal inline fun @receiver:StringRes Int.toAppString(): String {
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
return ZcashWalletApp.instance.resources.getInteger(this)}
fun Float.toPx() = this * Resources.getSystem().displayMetrics.density
fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density + 0.5f).toInt()
fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density + 0.5f).toInt()

View File

@ -24,7 +24,7 @@ fun View.disabledIf(isDisabled: Boolean) {
fun View.onClickNavTo(navResId: Int) {
setOnClickListener {
(context as? MainActivity)?.navController?.navigate(navResId)
(context as? MainActivity)?.safeNavigate(navResId)
?: throw IllegalStateException("Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}")
}

View File

@ -3,6 +3,27 @@ package cash.z.ecc.android.feedback
import cash.z.ecc.android.ZcashWalletApp
object Report {
object Send {
class SubmitFailure(private val errorCode: Int?, private val errorMessage: String?) : Feedback.Funnel("send.failure.submit") {
override fun toMap(): MutableMap<String, Any> {
return super.toMap().apply {
put("error.code", errorCode ?: -1)
put("error.message", errorMessage ?: "None")
}
}
}
class EncodingFailure(private val errorCode: Int?, private val errorMessage: String?) : Feedback.Funnel("send.failure.submit") {
override fun toMap(): MutableMap<String, Any> {
return super.toMap().apply {
put("error.code", errorCode ?: -1)
put("error.message", errorMessage ?: "None")
}
}
}
}
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"),

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.ui
import android.Manifest
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -17,6 +18,7 @@ import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
@ -34,6 +36,9 @@ import cash.z.ecc.android.feedback.LaunchMetric
import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.exception.CompactBlockProcessorException
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -41,6 +46,7 @@ import javax.inject.Inject
class MainActivity : AppCompatActivity() {
@Inject
lateinit var feedback: Feedback
@ -52,11 +58,14 @@ class MainActivity : AppCompatActivity() {
private val mediaPlayer: MediaPlayer = MediaPlayer()
private var snackbar: Snackbar? = null
private var dialog: Dialog? = null
lateinit var navController: NavController
lateinit var component: MainActivitySubcomponent
lateinit var synchronizerComponent: SynchronizerSubcomponent
var navController: NavController? = null
private val navInitListeners: MutableList<() -> Unit> = mutableListOf()
private val hasCameraPermission
get() = ContextCompat.checkSelfPermission(
this,
@ -118,19 +127,49 @@ class MainActivity : AppCompatActivity() {
private fun initNavigation() {
navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, _, _ ->
navController!!.addOnDestinationChangedListener { _, _, _ ->
// hide the keyboard anytime we change destinations
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
this@MainActivity.window.decorView.rootView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
for (listener in navInitListeners) {
listener()
}
navInitListeners.clear()
}
fun safeNavigate(@IdRes destination: Int) {
if (navController == null) {
navInitListeners.add {
try {
navController?.navigate(destination)
} catch (t: Throwable) {
twig("WARNING: during callback, did not navigate to destination: R.id.${resources.getResourceEntryName(destination)} due to: $t")
}
}
} else {
try {
navController?.navigate(destination)
} catch (t: Throwable) {
twig("WARNING: did not immediately navigate to destination: R.id.${resources.getResourceEntryName(destination)} due to: $t")
}
}
}
fun startSync(initializer: Initializer) {
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(initializer)
feedback.report(SYNC_START)
synchronizerComponent.synchronizer().start(lifecycleScope)
if (!::synchronizerComponent.isInitialized) {
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(initializer)
feedback.report(SYNC_START)
synchronizerComponent.synchronizer().let { synchronizer ->
synchronizer.onProcessorErrorHandler = ::onProcessorError
synchronizer.start(lifecycleScope)
}
} else {
twig("Ignoring request to start sync because sync has already been started!")
}
}
fun playSound(fileName: String) {
@ -218,6 +257,19 @@ class MainActivity : AppCompatActivity() {
}
}
fun showKeyboard(focusedView: View) {
twig("SHOWING KEYBOARD")
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(focusedView, InputMethodManager.SHOW_FORCED)
}
fun hideKeyboard() {
twig("HIDING KEYBOARD")
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(findViewById<View>(android.R.id.content).windowToken, 0)
}
/**
* @param popUpToInclusive the destination to remove from the stack before opening the camera.
* This only takes effect in the common case where the permission is granted.
@ -250,10 +302,31 @@ class MainActivity : AppCompatActivity() {
}
private fun openCamera(popUpToInclusive: Int? = null) {
navController.navigate(popUpToInclusive ?: R.id.action_global_nav_scan)
navController?.navigate(popUpToInclusive ?: R.id.action_global_nav_scan)
}
private fun onNoCamera() {
showSnackbar("Well, this is awkward. You denied permission for the camera.")
}
private fun onProcessorError(error: Throwable?): Boolean {
when (error) {
is CompactBlockProcessorException.Uninitialized -> {
if (dialog == null)
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Wallet Improperly Initialized")
.setMessage("This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. Please reimport using your backup seed phrase.")
.setCancelable(false)
.setPositiveButton("Exit") { dialog, _ ->
dialog.dismiss()
throw error
}
.show()
}
}
}
feedback.report(error)
return true
}
}

View File

@ -45,7 +45,7 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
fun onBackPressNavTo(navResId: Int) {
mainActivity?.onFragmentBackPressed(this) {
mainActivity?.navController?.navigate(navResId)
mainActivity?.safeNavigate(navResId)
}
}
}

View File

@ -14,6 +14,8 @@ class TransactionAdapter<T : ConfirmedTransaction> :
oldItem: T,
newItem: T
) = oldItem.minedHeight == newItem.minedHeight && oldItem.noteId == newItem.noteId
// bugfix: distinguish between self-transactions so they don't overwrite each other in the UI // TODO confirm that this is working, as intended
&& ((oldItem.raw == null && newItem.raw == null) || (oldItem.raw != null && newItem.raw != null && oldItem.raw!!.contentEquals(newItem.raw!!)))
override fun areContentsTheSame(
oldItem: T,

View File

@ -8,10 +8,12 @@ import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.MainActivity
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.isShielded
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.*
@ -60,8 +62,13 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineTwo = "Unknown"
}
}
// sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amount = "< 0.001"
else if (amount.length > 8) amount = "tap to view"
}
topText.text = lineOne
bottomText.text = lineTwo
amountText.text = amount
@ -74,7 +81,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private fun onTransactionClicked(transaction: ConfirmedTransaction) {
val txId = transaction.rawTransactionId.toTxId()
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
"Transaction: $txId"
"Transaction: $txId" +
"${if (transaction.toAddress != null) "\n\nto: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nmemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"
MaterialAlertDialogBuilder(itemView.context)
.setMessage(detailsMessage)
.setTitle("Transaction Details")

View File

@ -1,8 +1,6 @@
package cash.z.ecc.android.ui.home
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -12,18 +10,18 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.ext.disabledIf
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.*
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 cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.ext.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@ -64,7 +62,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// interact with user to create, backup and verify seed
// leads to a call to startSync(), later (after accounts are created from seed)
twig("Seed not found, therefore, launching seed creation flow")
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
mainActivity?.safeNavigate(R.id.action_nav_home_to_create_wallet)
} else {
twig("Found seed. Re-opening existing wallet")
mainActivity?.startSync(walletSetup.openWallet())
@ -136,9 +134,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
super.onResume()
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
viewModel.initializeMaybe()
twig("onResume (A)")
onClearAmount()
twig("onResume (B)")
viewModel.uiModels.scanReduce { old, new ->
onModelUpdated(old, new)
new
@ -148,18 +144,14 @@ twig("onResume (B)")
twig("exception while processing uiModels $e")
throw e
}.launchIn(resumedScope)
twig("onResume (C)")
// 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 {
twig("onResume (pre-fresh)")
viewModel.refreshBalance()
twig("onResume (post-fresh)")
}
twig("onResume (D)")
}
override fun onSaveInstanceState(outState: Bundle) {
@ -277,23 +269,16 @@ twig("onResume (D)")
private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
twig("onModelUpdated: $new")
if (binding.lottieButtonLoading.visibility != View.VISIBLE) binding.lottieButtonLoading.visibility = View.VISIBLE
uiModel = new
twig("onModelUpdated (A)")
if (old?.pendingSend != new.pendingSend) {
twig("onModelUpdated (B)")
setSendAmount(new.pendingSend)
twig("onModelUpdated (C)")
}
twig("onModelUpdated (D)")
// TODO: handle stopped and disconnected flows
setProgress(uiModel) // TODO: we may not need to separate anymore
twig("onModelUpdated (E)")
// if (new.status = SYNCING) onSyncing(new) else onSynced(new)
if (new.status == SYNCED) onSynced(new) else onSyncing(new)
twig("onModelUpdated (F)")
setSendEnabled(new.isSendEnabled)
twig("onModelUpdated (G) sendEnabled? ${new.isSendEnabled}")
twig("DONE onModelUpdated")
}
private fun onSyncing(uiModel: HomeViewModel.UiModel) {
@ -311,7 +296,7 @@ twig("onModelUpdated (G) sendEnabled? ${new.isSendEnabled}")
}
private fun onSend() {
mainActivity?.navController?.navigate(R.id.action_nav_home_to_send)
mainActivity?.safeNavigate(R.id.action_nav_home_to_send)
}
private fun onBannerAction(action: BannerAction) {
@ -323,7 +308,7 @@ twig("onModelUpdated (G) sendEnabled? ${new.isSendEnabled}")
.setCancelable(true)
.setPositiveButton("View Address") { dialog, _ ->
dialog.dismiss()
mainActivity?.navController?.navigate(R.id.action_nav_home_to_nav_receive)
mainActivity?.safeNavigate(R.id.action_nav_home_to_nav_receive)
}
.show()
// MaterialAlertDialogBuilder(activity)
@ -336,7 +321,7 @@ twig("onModelUpdated (G) sendEnabled? ${new.isSendEnabled}")
// }
// .setNegativeButton("View Address") { dialog, _ ->
// dialog.dismiss()
// mainActivity?.navController?.navigate(R.id.action_nav_home_to_nav_receive)
// mainActivity?.safeNavigate(R.id.action_nav_home_to_nav_receive)
// }
// .show()
}
@ -357,7 +342,7 @@ twig("onModelUpdated (G) sendEnabled? ${new.isSendEnabled}")
//
enum class BannerAction(val action: String) {
FUND_NOW("Fund Now"),
FUND_NOW(""),
CANCEL("Cancel"),
NONE(""),
CLEAR("clear");

View File

@ -5,6 +5,7 @@ import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.*
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.exception.RustLayerException
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
@ -57,7 +58,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
uiModels = synchronizer.run {
combine(status, processorInfo, balances, zec) { s, p, b, z->
UiModel(s, p, b.availableZatoshi, b.totalZatoshi, z)
}
}.onStart{ emit(UiModel()) }
}.conflate()
}
@ -71,7 +72,11 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
suspend fun refreshBalance() {
(synchronizer as SdkSynchronizer).refreshBalance()
try {
(synchronizer as SdkSynchronizer).refreshBalance()
} catch (e: RustLayerException.BalanceException) {
twig("Balance refresh failed. This is probably caused by a critical error but we'll give the app a chance to try to recover.")
}
}
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
@ -96,13 +101,10 @@ class HomeViewModel @Inject constructor() : ViewModel() {
if (lastDownloadRange.isEmpty()) {
100
} else {
twig("NUMERATOR: $lastDownloadedHeight - ${lastDownloadRange.first} + 1 = ${lastDownloadedHeight - lastDownloadRange.first + 1} block(s) downloaded")
twig("DENOMINATOR: ${lastDownloadRange.last} - ${lastDownloadRange.first} + 1 = ${lastDownloadRange.last - lastDownloadRange.first + 1} block(s) to download")
val progress =
(((lastDownloadedHeight - lastDownloadRange.first + 1).coerceAtLeast(0).toFloat() / (lastDownloadRange.last - lastDownloadRange.first + 1)) * 100.0f).coerceAtMost(
100.0f
).roundToInt()
twig("RESULT: $progress")
progress
}
}
@ -112,10 +114,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
if (lastScanRange.isEmpty()) {
100
} else {
twig("NUMERATOR: ${lastScannedHeight - lastScanRange.first + 1} block(s) scanned")
twig("DENOMINATOR: ${lastScanRange.last - lastScanRange.first + 1} block(s) to scan")
val progress = (((lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0).toFloat() / (lastScanRange.last - lastScanRange.first + 1)) * 100.0f).coerceAtMost(100.0f).roundToInt()
twig("RESULT: $progress")
progress
}
}

View File

@ -15,9 +15,7 @@ class MagicSnakeLoader(
var isSynced: Boolean = false
set(value) {
twig("ZZZ isSynced=$value isStarted=$isStarted")
if (value && !isStarted) {
twig("ZZZ isSynced=$value TURBO sync")
lottie.progress = 1.0f
field = value
return
@ -25,19 +23,16 @@ class MagicSnakeLoader(
// it is started but it hadn't reached the synced state yet
if (value && !field) {
twig("ZZZ synced was $field but now is $value so playing to completion since we are now synced")
field = value
playToCompletion()
} else {
field = value
twig("ZZZ isSynced=$value and lottie.progress=${lottie.progress}")
}
}
var scanProgress: Int = 0
set(value) {
field = value
twig("ZZZ scanProgress=$value")
if (value > 0) {
startMaybe()
onScanUpdated()
@ -47,7 +42,6 @@ class MagicSnakeLoader(
var downloadProgress: Int = 0
set(value) {
field = value
twig("ZZZ downloadProgress=$value")
if (value > 0) startMaybe()
}
@ -56,25 +50,12 @@ class MagicSnakeLoader(
if (!isSynced && !isStarted) lottie.postDelayed({
// after some delay, if we're still not synced then we better start animating (unless we already are)!
if (!isSynced && isPaused) {
twig("ZZZ yes start!")
lottie.resumeAnimation()
isPaused = false
isStarted = true
} else {
twig("ZZZ I would have started but we're already synced!")
}
}, 200L).also { twig("ZZZ startMaybe???") }
}, 200L)
}
// set(value) {
// field = value
// if (value in 1..99 && isStopped) {
// lottie.playAnimation()
// isStopped = false
// } else if (value >= 100) {
// isStopped = true
// }
// }
private val isDownloading get() = downloadProgress in 1..99
private val isScanning get() = scanProgress in 1..99
@ -83,25 +64,11 @@ class MagicSnakeLoader(
lottie.addAnimatorUpdateListener(this)
}
// downloading = true
// lottieAnimationView.playAnimation()
// lottieAnimationView.addAnimatorUpdateListener { valueAnimator ->
// // Set animation progress
// val progress = (valueAnimator.animatedValue as Float * 100).toInt()
// progressTv.text = "Progress: $progress%"
//
// if (downloading && progress >= 40) {
// lottieAnimationView.progress = 0f
// }
// }
override fun onAnimationUpdate(animation: ValueAnimator) {
if (isSynced || isPaused) {
// playToCompletion()
return
}
twig("ZZZ")
twig("ZZZ\t\tonAnimationUpdate(${animation.animatedValue})")
// if we are scanning, then set the animation progress, based on the scan progress
// if we're not scanning, then we're looping
@ -112,7 +79,6 @@ class MagicSnakeLoader(
private val acceptablePauseFrames = arrayOf(33,34,67,68,99)
private fun applyScanProgress(frame: Int) {
twig("ZZZ applyScanProgress($frame) : isPaused=$isPaused isStarted=$isStarted min=${lottie.minFrame} max=${lottie.maxFrame}")
// don't hardcode the progress until the loop animation has completed, cleanly
if (isPaused) {
onScanUpdated()
@ -126,7 +92,6 @@ class MagicSnakeLoader(
}
private fun onScanUpdated() {
twig("ZZZ onScanUpdated : isPaused=$isPaused")
if (isSynced) {
// playToCompletion()
return
@ -137,7 +102,6 @@ class MagicSnakeLoader(
val scanRange = scanningEndFrame - scanningStartFrame
val scanRangeProgress = scanProgress.toFloat() / 100.0f * scanRange.toFloat()
lottie.progress = (scanningStartFrame.toFloat() + scanRangeProgress) / totalFrames
twig("ZZZ onScanUpdated : scanRange=$scanRange scanRangeProgress=$scanRangeProgress lottie.progress=${(scanningStartFrame.toFloat() + scanRangeProgress)}/$totalFrames=${lottie.progress}")
}
}
@ -160,17 +124,14 @@ class MagicSnakeLoader(
}
private fun allowLoop(frame: Int) {
twig("ZZZ allowLoop($frame) : isPaused=$isPaused")
unpause()
if (frame >= scanningStartFrame) {
twig("ZZZ resetting to 0f (LOOPING)")
lottie.progress = 0f
}
}
fun unpause() {
if (isPaused) {
twig("ZZZ unpausing")
lottie.resumeAnimation()
isPaused = false
}
@ -178,7 +139,6 @@ class MagicSnakeLoader(
fun pause() {
if (!isPaused) {
twig("ZZZ pausing")
lottie.pauseAnimation()
isPaused = true
}

View File

@ -79,7 +79,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
if (viewModel.isNotValid(qrContent)) image.close() // continue scanning
else {
sendViewModel.toAddress = qrContent
mainActivity?.navController?.navigate(R.id.action_nav_scan_to_nav_send_address)
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send_address)
}
}
}

View File

@ -99,7 +99,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
binding.inputZcashAmount.convertZecToZatoshi()?.let { sendViewModel.zatoshiAmount = it }
sendViewModel.validate(maxZatoshi).onFirstWith(resumedScope) {
if (it == null) {
mainActivity?.navController?.navigate(R.id.action_nav_send_address_to_send_memo)
mainActivity?.safeNavigate(R.id.action_nav_send_address_to_send_memo)
} else {
resumedScope.launch {
binding.textAddressError.text = it

View File

@ -42,6 +42,6 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
}
private fun onSend() {
mainActivity?.navController?.navigate(R.id.action_nav_send_confirm_to_send_final)
mainActivity?.safeNavigate(R.id.action_nav_send_confirm_to_send_final)
}
}

View File

@ -44,7 +44,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
}
binding.textConfirmation.text =
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel.toAddress.toAbbreviatedAddress()}"
sendViewModel.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
@ -91,6 +91,15 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
pendingTransaction.isCreating() -> "Creating transaction . . ."
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
}
// TODO: make this error tracking easier to use and more spiffy
if (pendingTransaction?.isFailedSubmit() == true) {
sendViewModel.feedback.report(Report.Send.SubmitFailure(pendingTransaction?.errorCode, pendingTransaction?.errorMessage))
}
if (pendingTransaction?.isFailedEncoding() == true) {
sendViewModel.feedback.report(Report.Send.EncodingFailure(pendingTransaction?.errorCode, pendingTransaction?.errorMessage))
}
twig("Pending TX (id: ${pendingTransaction?.id} Updated with message: $message")
binding.textStatus.apply {
text = "$message"
@ -106,7 +115,9 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
sendViewModel.reset()
}
} catch(t: Throwable) {
twig("ERROR: error while handling pending transaction update! $t")
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
Crashlytics.log(message)
Crashlytics.logException(t)
}
}

View File

@ -4,13 +4,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.widget.doAfterTextChanged
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.ui.base.BaseFragment
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
@ -28,6 +26,10 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
binding.buttonSkip.setOnClickListener {
onBottomButton()
}
binding.clearMemo.setOnClickListener {
onClearMemo()
}
R.id.action_nav_send_memo_to_nav_send_address.let {
binding.backButtonHitArea.onClickNavTo(it)
onBackPressNavTo(it)
@ -37,14 +39,15 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
onIncludeMemo(binding.checkIncludeAddress.isChecked)
}
binding.inputMemo.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
binding.inputMemo.let { memo ->
memo.onEditorActionDone {
onTopButton()
true
} else {
false
}
memo.doAfterTextChanged {
binding.clearMemo.goneIf(memo.text.isEmpty())
}
}
sendViewModel.afterInitFromAddress {
binding.textIncludedAddress.text = "sent from ${sendViewModel.fromAddress}"
}
@ -54,19 +57,23 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
applyModel()
}
private fun onClearMemo() {
binding.inputMemo.setText("")
}
private fun applyModel() {
sendViewModel.isShielded.let { isShielded ->
binding.groupShielded.goneIf(!isShielded)
binding.groupTransparent.goneIf(isShielded)
binding.textIncludedAddress.goneIf(!sendViewModel.includeFromAddress)
if (isShielded) {
binding.inputMemo.setText(sendViewModel.memo)
binding.checkIncludeAddress.isChecked = sendViewModel.includeFromAddress
binding.buttonNext.text = "ADD MEMO"
binding.buttonSkip.text = "SEND WITHOUT MEMO"
binding.buttonSkip.text = "OMIT MEMO"
} else {
binding.buttonNext.text = "WAIT, GO BACK"
binding.buttonNext.text = "GO BACK"
binding.buttonSkip.text = "PROCEED"
binding.sadTitle.text = binding.sadTitle.text.toString().toColoredSpan(R.color.colorPrimary, "sad")
}
}
}
@ -74,7 +81,11 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
private fun onIncludeMemo(checked: Boolean) {
binding.textIncludedAddress.goneIf(!checked)
sendViewModel.includeFromAddress = checked
if (checked) binding.inputMemo.setHint("") else binding.inputMemo.setHint("Add a memo here")
binding.textInfoShielded.text = if (checked) {
getString(R.string.send_memo_included_message)
} else {
getString(R.string.send_memo_excluded_message)
}
}
private fun onTopButton() {
@ -82,7 +93,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
sendViewModel.memo = binding.inputMemo.text.toString()
onNext()
} else {
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_nav_send_address)
mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_nav_send_address)
}
}
@ -94,6 +105,6 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
}
private fun onNext() {
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm)
}
}

View File

@ -81,8 +81,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit("Please enter a valid address")
}
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> {
emit("Too little! Please enter at least 0.0001")
zatoshiAmount <= 1 -> {
emit("Too little! Please enter at least 1 Zatoshi.")
}
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit( "Too much! Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}")

View File

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

View File

@ -69,12 +69,17 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
walletSetup.checkSeed().onEach {
when(it) {
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
mainActivity?.navController?.navigate(R.id.nav_backup)
mainActivity?.safeNavigate(R.id.nav_backup)
}
}
}.launchIn(lifecycleScope)
}
override fun onResume() {
super.onResume()
mainActivity?.hideKeyboard()
}
private fun onSkip(count: Int) {
when (count) {
1 -> {
@ -94,7 +99,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
private fun onRestoreWallet() {
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
mainActivity?.safeNavigate(R.id.action_nav_landing_to_nav_restore)
}
// AKA import wallet
@ -133,7 +138,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
private fun onBackupWallet() {
skipCount = 0
mainActivity?.navController?.navigate(R.id.action_nav_landing_to_nav_backup)
mainActivity?.safeNavigate(R.id.action_nav_landing_to_nav_backup)
}
private fun onEnterWallet() {

View File

@ -0,0 +1,193 @@
package cash.z.ecc.android.ui.setup
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.SystemClock
import android.text.InputType
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentRestoreBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tylersuehr.chips.Chip
import com.tylersuehr.chips.ChipsAdapter
import com.tylersuehr.chips.SeedWordAdapter
import kotlinx.coroutines.launch
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private lateinit var seedWordRecycler: RecyclerView
private var seedWordAdapter: SeedWordAdapter? = null
override fun inflate(inflater: LayoutInflater): FragmentRestoreBinding =
FragmentRestoreBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
seedWordRecycler = binding.chipsInput.findViewById<RecyclerView>(R.id.chips_recycler)
seedWordAdapter = SeedWordAdapter(seedWordRecycler.adapter as ChipsAdapter).onDataSetChanged {
onChipsModified()
}.also { onChipsModified() }
seedWordRecycler.adapter = seedWordAdapter
binding.chipsInput.apply {
setFilterableChipList(getChips())
setDelimiter("[ ;,]", true)
}
binding.buttonDone.setOnClickListener {
onDone()
}
binding.buttonSuccess.setOnClickListener {
onEnterWallet()
}
binding.textSubtitle.setOnClickListener {
seedWordAdapter!!.editText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity?.onFragmentBackPressed(this) {
if (seedWordAdapter == null || seedWordAdapter?.itemCount == 1) {
onExit()
} else {
MaterialAlertDialogBuilder(activity)
.setMessage("Are you sure? For security, the words that you have entered will be cleared!")
.setTitle("Abort?")
.setPositiveButton("Stay") { dialog, _ ->
dialog.dismiss()
}
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
onExit()
}
.show()
}
}
}
override fun onResume() {
super.onResume()
// Require one less tap to enter the seed words
touchScreenForUser()
}
private fun onExit() {
hideAutoCompleteWords()
mainActivity?.hideKeyboard()
mainActivity?.navController?.popBackStack()
}
private fun onEnterWallet() {
mainActivity?.safeNavigate(R.id.action_nav_restore_to_nav_home)
}
private fun onDone() {
mainActivity?.hideKeyboard()
val seedPhrase = binding.chipsInput.selectedChips.joinToString(" ") {
it.title
}
var birthday = binding.root.findViewById<TextView>(R.id.input_birthdate).text.toString()
.let { birthdateString ->
if (birthdateString.isNullOrEmpty()) ZcashSdk.SAPLING_ACTIVATION_HEIGHT else birthdateString.toInt()
}.coerceAtLeast(ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
importWallet(seedPhrase, birthday)
}
private fun importWallet(seedPhrase: String, birthday: Int) {
mainActivity?.hideKeyboard()
mainActivity?.apply {
lifecycleScope.launch {
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
// bugfix: if the user proceeds before the synchronizer is created the app will crash!
binding.buttonSuccess.isEnabled = true
}
playSound("sound_receive_small.mp3")
vibrateSuccess()
}
binding.groupDone.visibility = View.GONE
binding.groupStart.visibility = View.GONE
binding.groupSuccess.visibility = View.VISIBLE
binding.buttonSuccess.isEnabled = false
}
private fun onChipsModified() {
twig("onChipsModified")
seedWordAdapter?.editText?.apply {
postDelayed({
requestFocus()
},40L)
}
setDoneEnabled()
view!!.postDelayed({
mainActivity!!.showKeyboard(seedWordAdapter!!.editText)
seedWordAdapter?.editText?.requestFocus()
}, 500L)
}
private fun setDoneEnabled() {
val count = seedWordAdapter?.itemCount ?: 0
binding.groupDone.goneIf(count <= 24)
}
private fun hideAutoCompleteWords() {
seedWordAdapter?.editText?.setText("")
}
private fun getChips(): List<Chip> {
return resources.getStringArray(R.array.word_list).map {
SeedWordChip(it)
}
}
private fun touchScreenForUser() {
seedWordAdapter?.editText?.apply {
postDelayed({
seedWordAdapter?.editText?.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
dispatchTouchEvent(motionEvent(ACTION_DOWN))
dispatchTouchEvent(motionEvent(ACTION_UP))
}, 100L)
}
}
private fun motionEvent(action: Int) = SystemClock.uptimeMillis().let { now ->
MotionEvent.obtain(now, now, action, 0f, 0f, 0)
}
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
return false
}
}
class SeedWordChip(val word: String, var index: Int = -1) : Chip() {
override fun getSubtitle(): String? = null//"subtitle for $word"
override fun getAvatarDrawable(): Drawable? = null
override fun getId() = index
override fun getTitle() = word
override fun getAvatarUri() = null
}

View File

@ -0,0 +1,94 @@
package com.tylersuehr.chips
import android.content.Context
import android.text.TextUtils
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.setup.SeedWordChip
import cash.z.wallet.sdk.ext.twig
class SeedWordAdapter : ChipsAdapter {
constructor(existingAdapter: ChipsAdapter) : super(existingAdapter.mDataSource, existingAdapter.mEditText, existingAdapter.mOptions)
val editText = mEditText
private var onDataSetChangedListener: (() -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == CHIP) SeedWordHolder(SeedWordChipView(parent.context))
else object : RecyclerView.ViewHolder(mEditText) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == CHIP) { // Chips
// Display the chip information on the chip view
(holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position);
} else {
val size = mDataSource.selectedChips.size
mEditText.hint = if (size < 3) {
mEditText.isEnabled = true
mEditText.setHintTextColor(R.color.text_light_dimmed.toAppColor())
val ordinal = when(size) {2 -> "3rd"; 1 -> "2nd"; else -> "1st"}
"Enter $ordinal seed word"
} else if(size >= 24) {
mEditText.setHintTextColor(R.color.zcashGreen.toAppColor())
mEditText.isEnabled = false
"done"
} else {
mEditText.isEnabled = true
mEditText.setHintTextColor(R.color.zcashYellow.toAppColor())
"${size + 1}"
}
}
}
override fun onChipDataSourceChanged() {
super.onChipDataSourceChanged()
twig("onChipDataSourceChanged")
onDataSetChangedListener?.invoke()
}
fun onDataSetChanged(block: () -> Unit): SeedWordAdapter {
onDataSetChangedListener = block
return this
}
override fun onKeyboardActionDone(text: String?) {
if (TextUtils.isEmpty(text)) return
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply {
postDelayed({
setText("")
requestFocus()
}, 50L)
}
}
}
override fun onKeyboardDelimiter(text: String) {
twig("onKeyboardDelimiter: $text ${mDataSource.filteredChips.size}")
if (mDataSource.filteredChips.size > 0) {
onKeyboardActionDone((mDataSource.filteredChips.first() as SeedWordChip).word)
}
}
private inner class SeedWordHolder(chipView: SeedWordChipView) : ChipsAdapter.ChipHolder(chipView) {
val seedChipView = super.chipView as SeedWordChipView
}
private inner class SeedWordChipView(context: Context) : ChipView(context) {
private val indexView: TextView = findViewById(R.id.chip_index)
fun bind(chip: Chip, index: Int) {
super.inflateFromChip(chip)
indexView.text = (index + 1).toString()
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:strokeWidth="1"
android:fillType="evenOdd"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="92.96752"
android:centerX="54"
android:centerY="36.01165"
android:type="radial">
<item android:offset="0" android:color="#FF3F3F4F"/>
<item android:offset="1" android:color="#FF000000"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,102 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M78.6,53.6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M28.6,53.6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M54.4,78.6m-0,-1.6a1.6,1.6 0,1 1,-0 3.2a1.6,1.6 0,1 1,-0 -3.2"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M54.4,28.6m-0,-1.6a1.6,1.6 0,1 1,-0 3.2a1.6,1.6 0,1 1,-0 -3.2"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M77.8,54.601C77.8,54.79 77.799,54.963 77.799,54.963L46.564,54.967C46.755,58.942 50.012,62.117 53.999,62.117C56.367,62.117 58.475,60.995 59.842,59.255L60.739,59.255C59.264,61.419 56.794,62.843 53.999,62.843C49.752,62.843 46.262,59.557 45.872,55.375L45.862,55.261C45.855,55.164 45.855,55.065 45.851,54.966L39.9,54.967C39.769,55.519 39.351,55.953 38.814,56.109C39.569,63.892 46.087,69.996 54,69.996C60.81,69.996 66.592,65.475 68.553,59.254L69.305,59.254C67.32,65.881 61.211,70.721 54,70.721C45.704,70.721 38.873,64.313 38.097,56.149C37.49,56.035 37.009,55.569 36.866,54.967L32.082,54.968C31.949,55.527 31.522,55.965 30.974,56.116C31.752,68.246 41.779,77.875 54,77.875C65.147,77.875 74.471,69.865 76.615,59.254L77.346,59.254C75.19,70.267 65.545,78.6 54,78.6C41.392,78.6 31.048,68.661 30.254,56.146C29.538,56.001 29,55.364 29,54.6C29,53.836 29.539,53.199 30.254,53.054C31.048,40.539 41.391,30.6 54,30.6C67.003,30.601 77.597,41.172 77.791,54.239C77.793,54.36 77.8,54.48 77.8,54.601ZM77.072,54.239C76.878,41.572 66.606,31.327 53.999,31.327C41.778,31.327 31.75,40.957 30.973,53.085C31.522,53.236 31.95,53.677 32.082,54.239L36.863,54.239C37.005,53.635 37.487,53.167 38.095,53.053C38.871,44.888 45.702,38.48 53.999,38.48C61.069,38.48 67.078,43.134 69.182,49.561L68.422,49.561C66.347,43.541 60.667,39.206 53.999,39.206C46.086,39.206 39.568,45.31 38.813,53.092C39.351,53.25 39.77,53.685 39.9,54.239L45.845,54.239C45.852,54.091 45.86,53.961 45.86,53.961C46.186,49.716 49.706,46.36 53.999,46.36C56.623,46.36 58.955,47.617 60.451,49.561L59.513,49.561C58.148,48.045 56.185,47.085 53.999,47.085C50.01,47.085 46.752,50.263 46.562,54.239L46.562,54.239L77.072,54.239Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillAlpha="0.5"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M78.6,53.6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M28.6,53.6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M54.4,78.6m-0,-1.6a1.6,1.6 0,1 1,-0 3.2a1.6,1.6 0,1 1,-0 -3.2"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M54.4,28.6m-0,-1.6a1.6,1.6 0,1 1,-0 3.2a1.6,1.6 0,1 1,-0 -3.2"
android:strokeAlpha="0"
android:strokeWidth="1"
android:fillColor="#9013FE"
android:fillType="evenOdd"
android:strokeColor="#00000000"
android:fillAlpha="0"/>
<path
android:pathData="M77.8,54.601C77.8,54.79 77.799,54.963 77.799,54.963L46.564,54.967C46.755,58.942 50.012,62.117 53.999,62.117C56.367,62.117 58.475,60.995 59.842,59.255L60.739,59.255C59.264,61.419 56.794,62.843 53.999,62.843C49.752,62.843 46.262,59.557 45.872,55.375L45.862,55.261C45.855,55.164 45.855,55.065 45.851,54.966L39.9,54.967C39.769,55.519 39.351,55.953 38.814,56.109C39.569,63.892 46.087,69.996 54,69.996C60.81,69.996 66.592,65.475 68.553,59.254L69.305,59.254C67.32,65.881 61.211,70.721 54,70.721C45.704,70.721 38.873,64.313 38.097,56.149C37.49,56.035 37.009,55.569 36.866,54.967L32.082,54.968C31.949,55.527 31.522,55.965 30.974,56.116C31.752,68.246 41.779,77.875 54,77.875C65.147,77.875 74.471,69.865 76.615,59.254L77.346,59.254C75.19,70.267 65.545,78.6 54,78.6C41.392,78.6 31.048,68.661 30.254,56.146C29.538,56.001 29,55.364 29,54.6C29,53.836 29.539,53.199 30.254,53.054C31.048,40.539 41.391,30.6 54,30.6C67.003,30.601 77.597,41.172 77.791,54.239C77.793,54.36 77.8,54.48 77.8,54.601ZM77.072,54.239C76.878,41.572 66.606,31.327 53.999,31.327C41.778,31.327 31.75,40.957 30.973,53.085C31.522,53.236 31.95,53.677 32.082,54.239L36.863,54.239C37.005,53.635 37.487,53.167 38.095,53.053C38.871,44.888 45.702,38.48 53.999,38.48C61.069,38.48 67.078,43.134 69.182,49.561L68.422,49.561C66.347,43.541 60.667,39.206 53.999,39.206C46.086,39.206 39.568,45.31 38.813,53.092C39.351,53.25 39.77,53.685 39.9,54.239L45.845,54.239C45.852,54.091 45.86,53.961 45.86,53.961C46.186,49.716 49.706,46.36 53.999,46.36C56.623,46.36 58.955,47.617 60.451,49.561L59.513,49.561C58.148,48.045 56.185,47.085 53.999,47.085C50.01,47.085 46.752,50.263 46.562,54.239L46.562,54.239L77.072,54.239Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillAlpha="0.5"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

View File

@ -2,6 +2,6 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp" />
<stroke android:width="1dp" android:color="#282828"/>
<stroke android:width="1dp" android:color="@color/background_banner_stroke"/>
<solid android:color="@color/background_banner"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="3dp" />
<stroke android:width="1dp" android:color="#282828"/>
<solid android:color="@color/background_banner"/>
</shape>

View File

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

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,65 @@
<?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:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="@dimen/chip_height"
android:background="@drawable/bg_chip_view"
android:clickable="true">
<com.tylersuehr.chips.CircleImageView
android:id="@+id/avatar"
android:layout_width="@dimen/chip_height"
android:layout_height="@dimen/chip_height"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/avatar" />
<TextView
android:id="@+id/chip_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:text="12"
android:textSize="12dp"
android:fontFamily="@font/inconsolata"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="4dp"
android:layout_marginStart="8dp"
android:textColor="@color/text_light_dimmed"
android:textSize="@dimen/chip_label_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/chip_index"
app:layout_constraintRight_toLeftOf="@+id/button_delete"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="artwork" />
<ImageButton
android:id="@+id/button_delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:alpha=".54"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/chip_delete_icon_20dp"
android:tint="@color/zcashWhite_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,49 @@
<?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="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:clickable="true">
<com.tylersuehr.chips.CircleImageView
android:id="@+id/image"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:src="@drawable/avatar"/>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="18dp"
android:includeFontPadding="false"
android:textColor="@color/selector_button_text_light_to_dimmed"
app:layout_constraintBottom_toTopOf="@+id/subtitle"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:alpha=".56"
android:textSize="@dimen/chip_label_text_size"
android:textColor="#212121"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintLeft_toRightOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Subtitle"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -278,7 +278,9 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/guide_keys"
app:layout_constraintBottom_toTopOf="@id/text_detail"
app:layout_constraintVertical_bias="0.38"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
android:visibility="invisible"
app:lottie_autoPlay="false"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_button_loading_new" />
@ -373,6 +375,7 @@
android:layout_height="wrap_content"
android:padding="12dp"
android:elevation="6dp"
android:layout_marginTop="12dp"
android:text="Wallet Details"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"
@ -380,8 +383,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon_detail"
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading"
app:layout_constraintVertical_bias="@dimen/ratio_golden_small" />
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading" />
<TextView
android:id="@+id/text_send_amount"

View File

@ -0,0 +1,187 @@
<?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">
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start"
android:paddingStart="32dp"
android:paddingTop="32dp"
android:text="Restoring from a backup"
android:textColor="@color/text_light"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="start"
android:paddingBottom="32dp"
android:paddingStart="32dp"
android:paddingTop="18dp"
android:text="You will need to enter all 24 seed words.\nDon't worry, we will find them as you type."
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title" />
<com.tylersuehr.chips.ChipsInputLayout
android:id="@+id/chips_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/zcashBlack_54"
android:hint="Enter the 1st seed word..."
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:scrollbarStyle="outsideOverlay"
android:textColor="@color/text_light"
android:textColorHint="#757575"
app:hideKeyboardOnChipClick="false"
app:allowCustomChips="false"
app:chip_showDelete="false"
app:chip_showDetails="true"
app:chip_textColor="@color/text_light_dimmed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_subtitle" />
<View
android:id="@+id/divider_top"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/chips_input" />
<View
android:id="@+id/divider_bottom"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/chips_input"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_birthdate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="Enter wallet birthday height (recommended)"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:helperText="e.g. 419,200. This determines where to start scanning for transactions. Leave it blank to scan from the beginning, which takes a while."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_bottom"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_birthdate"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLength="6"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Import"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_birthdate" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.33333"
app:layout_constraintWidth_percent="0.4053398058"
app:lottie_autoPlay="true"
app:lottie_loop="false"
app:lottie_rawRes="@raw/lottie_success" />
<!-- <ImageView-->
<!-- android:id="@+id/icon_success"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:tint="#00FF00"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintDimensionRatio="W,1:1"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0.33333"-->
<!-- app:layout_constraintWidth_percent="0.4053398058"-->
<!-- app:srcCompat="@drawable/ic_check_shield" />-->
<TextView
android:id="@+id/text_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="12dp"
android:text="Success"
android:textColor="@color/text_light"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_success" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="View Wallet"
android:textColor="@color/text_dark"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_success"
/>
<androidx.constraintlayout.widget.Group
android:id="@+id/group_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="lottie_success, text_success, button_success" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="button_done, text_layout_birthdate" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="text_title, text_subtitle, chips_input, divider_bottom, divider_top" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -33,33 +33,57 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<EditText
android:id="@+id/input_memo"
<View
android:id="@+id/background_memo"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_banner"
android:elevation="6dp"
android:gravity="top"
android:hint="Add a memo here"
android:maxLines="3"
android:imeActionLabel="add memo"
android:imeOptions="actionDone"
android:inputType="textImeMultiLine"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="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" />
app:layout_constraintWidth_percent="0.8" />
<EditText
android:id="@+id/input_memo"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/transparent"
android:elevation="6dp"
android:gravity="top"
android:scrollbars="vertical"
android:scrollbarStyle="outsideOverlay"
tools:text="this\nis\nsome\ntext\nthat\nspans\nmany\nlines"
android:hint="Add a memo here"
android:imeActionLabel="add memo"
android:imeOptions="actionDone"
android:inputType="textMultiLine"
android:maxLength="512"
android:paddingBottom="8dp"
android:paddingEnd="32dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintBottom_toTopOf="@id/text_included_address"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
app:layout_constraintTop_toTopOf="@id/background_memo" />
<ImageView
android:id="@+id/clear_memo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:elevation="6dp"
android:src="@drawable/ic_close_black_24dp"
android:tint="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintTop_toTopOf="@id/background_memo" />
<TextView
android:id="@+id/text_included_address"
@ -70,9 +94,9 @@
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/input_memo"
app:layout_constraintEnd_toEndOf="@id/input_memo"
app:layout_constraintStart_toStartOf="@id/input_memo"
app:layout_constraintBottom_toBottomOf="@id/background_memo"
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
tools:text="sent from z23lk4jjl2k3jl43kkj542l3kl4hj2l3k1j41l2kjk423lkj423lklhk2jrhiuhrh2j4hh2hkj23hkj4" />
<View
@ -94,82 +118,79 @@
android:layout_marginTop="16dp"
android:padding="0dp"
android:text="Include your sending address in memo"
app:layout_constraintStart_toStartOf="@+id/input_memo"
app:layout_constraintTop_toBottomOf="@+id/input_memo" />
app:layout_constraintStart_toStartOf="@+id/background_memo"
app:layout_constraintTop_toBottomOf="@+id/background_memo" />
<TextView
android:id="@+id/text_info_shielded"
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"
android:text="Your transaction is shielded and your address is not available to recipient."
app:layout_constraintEnd_toEndOf="@id/background_memo"
app:layout_constraintStart_toStartOf="@id/background_memo"
app:layout_constraintTop_toBottomOf="@id/check_include_address" />
<ImageView
android:id="@+id/sad_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="36dp"
android:layout_height="36dp"
android:elevation="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintBottom_toTopOf="@id/sad_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.17"
app:layout_constraintWidth_percent="0.68"
app:srcCompat="@drawable/ic_sadzebra" />
app:srcCompat="@drawable/ic_info_24dp"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/sad_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="16dp"
android:layout_marginTop="16dp"
android:drawableTint="@color/text_light_dimmed"
android:text="You are going to make the zebra sad."
android:gravity="center"
android:paddingEnd="32dp"
android:paddingStart="32dp"
android:text="Sending to a transparent address will let everyone see the amount and the recipient."
android:textColor="@color/text_light"
android:textSize="18dp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_icon"
app:layout_constraintBottom_toTopOf="@id/sad_description"
app:layout_constraintVertical_chainStyle="packed"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_icon" />
<TextView
android:id="@+id/sad_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:drawablePadding="16dp"
android:drawableTint="@color/text_light_dimmed"
android:gravity="center"
android:text="Heads up! You are sending to a transparent address, which reduces your privacy and does not support memos."
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Your privacy is protected, if you use your wallet to store your funds. Using the zECC wallet to pass funds through a z-address could compromise your privacy."
android:textColor="@color/text_light"
android:textSize="18dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/sad_checkbox"
app:layout_constraintEnd_toEndOf="@id/sad_title"
app:layout_constraintHeight_percent="0.2"
app:layout_constraintStart_toStartOf="@id/sad_title"
app:layout_constraintTop_toBottomOf="@id/sad_title"
app:layout_constraintBottom_toTopOf="@id/sad_checkbox"
app:layout_constraintVertical_bias="0.5263" />
<CheckBox
android:id="@+id/sad_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Don't show this again"
android:checked="true"
app:layout_constraintStart_toStartOf="parent"
android:text="Don't show this again"
app:layout_constraintBottom_toTopOf="@id/button_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_description"
app:layout_constraintBottom_toTopOf="@id/button_next"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sad_description" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
@ -177,14 +198,14 @@
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" />
app:layout_constraintWidth_percent="0.68"
tools:text="Add Memo" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_skip"
@ -192,22 +213,23 @@
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" />
app:layout_constraintTop_toBottomOf="@id/button_next"
tools:text="Omit memo" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:visibility="visible"
app:constraint_referenced_ids="sad_description, sad_icon, sad_title, sad_checkbox" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_shielded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="input_memo, check_include_address, text_included_address, text_info_shielded"
tools:visibility="visible" />
app:constraint_referenced_ids="clear_memo, background_memo, input_memo, check_include_address, text_info_shielded"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -156,6 +156,9 @@
app:destination="@id/nav_backup"
app:popUpTo="@id/nav_landing"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_nav_landing_to_nav_restore"
app:destination="@id/nav_restore" />
</fragment>
<fragment
@ -164,6 +167,17 @@
tools:layout="@layout/fragment_backup" >
</fragment>
<fragment
android:id="@+id/nav_restore"
android:name="cash.z.ecc.android.ui.setup.RestoreFragment"
tools:layout="@layout/fragment_restore" >
<action
android:id="@+id/action_nav_restore_to_nav_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_landing"
app:popUpToInclusive="true" />
</fragment>
<!-- -->
<!-- Global actions -->

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@
but have a more useful name for use in code -->
<color name="background_banner">@color/zcashBlack_dark</color>
<color name="background_banner_stroke">#282828</color>
<color name="scan_overlay_background">@color/zcashBlack_87</color>
<color name="spacer">#1FBB666A</color>
<color name="text_send_amount_disabled">@color/text_light</color>

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">Zcash Wallet</string>
<string name="app_name">zECC Wallet</string>
<string name="receive_address_title">Your Shielded Address</string>
@ -8,4 +8,7 @@
<!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
<string name="send_hint_input_zcash_amount">Enter an amount to send</string>
<string name="send_memo_excluded_message">Your transaction is shielded and your address is not available to the recipient</string>
<string name="send_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</string>
</resources>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,9 @@
import cash.z.ecc.android.Deps
buildscript {
ext {
kotlin_version = '1.3.61'
}
repositories {
google()
jcenter()
@ -9,7 +12,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0-rc01'
classpath 'com.android.tools.build:gradle:3.6.0-rc02'
classpath 'com.google.gms:google-services:4.3.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}"
classpath 'io.fabric.tools:gradle:1.31.2'
@ -21,6 +24,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}

View File

@ -3,7 +3,7 @@ package cash.z.ecc.android
object Deps {
// For use in the top-level build.gradle which gives an error when provided
// `Deps.Kotlin.version` directly
const val kotlinVersion = "1.3.60"
const val kotlinVersion = "1.3.61"
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
@ -37,7 +37,7 @@ object Deps {
const val INJECT = "javax.inject:javax.inject:1"
}
object Kotlin : Version(kotlinVersion) {
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version"
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version"
object Coroutines : Version("1.3.2") {
val ANDROID = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
val CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"

View File

@ -6,10 +6,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.StringBuilder
import kotlin.coroutines.coroutineContext
class Feedback(capacity: Int = 256) {
@ -145,8 +143,8 @@ class Feedback(capacity: Int = 256) {
*
* @param error the uncaught exception that occurred.
*/
fun report(error: Throwable?): Feedback {
return report(Crash(error))
fun report(error: Throwable?, fatal: Boolean = false): Feedback {
return report(Crash(error, fatal))
}
/**
@ -199,6 +197,14 @@ class Feedback(capacity: Int = 256) {
}
}
abstract class Funnel(override val key: String) : Action {
override fun toMap(): MutableMap<String, Any> {
return mutableMapOf(
"key" to key
)
}
}
interface Keyed<T> {
val key: T
}
@ -225,17 +231,19 @@ class Feedback(capacity: Int = 256) {
}
}
data class Crash(val error: Throwable?) : Action {
data class Crash(val error: Throwable? = null, val fatal: Boolean = true) : Action {
override val key: String = "crash"
override fun toMap(): Map<String, Any> {
return mutableMapOf<String, Any>(
"fatal" to fatal,
"message" to (error?.message ?: "None"),
"cause" to (error?.cause?.toString() ?: "None"),
"cause.cause" to (error?.cause?.cause?.toString() ?: "None"),
"cause.cause.cause" to (error?.cause?.cause?.cause?.toString() ?: "None")
).apply { putAll(super.toMap()); putAll(error.stacktraceToMap()) }
}
override fun toString() = "App crashed due to: $error"
override fun toString() = "App ${if (fatal) "crashed due to" else "caught error"}: $error"
}
}

View File

@ -1,23 +1,18 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
#Wed Jan 29 09:45:08 EST 2020
kotlin.code.style=official
dagger.fastInit=enabled
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
dagger.fastInit=enabled

View File

@ -42,6 +42,9 @@ dependencies {
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.CORE_KTX
// Zcash
implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0'
implementation "de.adorsys.android:securestoragelibrary:1.2.2"
androidTestImplementation Deps.Test.Android.JUNIT

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.lockbox
import android.content.Context
import cash.z.android.plugin.LockBoxPlugin
import de.adorsys.android.securestoragelibrary.SecurePreferences
import java.nio.ByteBuffer
import java.nio.CharBuffer
@ -8,7 +9,7 @@ import java.nio.charset.StandardCharsets
import java.util.*
import javax.inject.Inject
class LockBox @Inject constructor(private val appContext: Context) : LockBoxProvider {
class LockBox @Inject constructor(private val appContext: Context) : LockBoxPlugin {
override fun setBoolean(key: String, value: Boolean) {
SecurePreferences.setValue(appContext, key, value)

View File

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

View File

@ -6,6 +6,9 @@ dependencies {
implementation Deps.JavaX.INJECT
implementation Deps.Kotlin.STDLIB
// Zcash
implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0'
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'

View File

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

View File

@ -1,5 +1,6 @@
package cash.z.ecc.kotlin.mnemonic
import cash.z.android.plugin.MnemonicPlugin
import io.github.novacrypto.bip39.MnemonicGenerator
import io.github.novacrypto.bip39.SeedCalculator
import io.github.novacrypto.bip39.Words
@ -10,7 +11,7 @@ import javax.inject.Inject
// 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)
class Mnemonics @Inject constructor(): MnemonicProvider {
class Mnemonics @Inject constructor(): MnemonicPlugin {
override fun nextEntropy(): ByteArray {
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {

View File

@ -1,3 +1,4 @@
rootProject.name='Zcash Wallet'
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':sdk'
project(":sdk").projectDir = file("../zcash-android-wallet-sdk")
include ':app', ':qrecycler', ':feedback', ':mnemonic', ':lockbox', ':sdk', ':chipsinputlayout'
project(":sdk").projectDir = file("../zcash-android-wallet-sdk")
project(":chipsinputlayout").projectDir = file("../../clones/chips-input-layout/library")