Merge pull request #78 from zcash/release/sprint-2
Release/sprint-20-05
|
@ -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
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
|
|||
|
||||
fun onBackPressNavTo(navResId: Int) {
|
||||
mainActivity?.onFragmentBackPressed(this) {
|
||||
mainActivity?.navController?.navigate(navResId)
|
||||
mainActivity?.safeNavigate(navResId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)}")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 25 KiB |
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|