Merge pull request #65 from zcash/task/stress-test-fixes
Task/stress test fixes
|
@ -5,11 +5,13 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
//apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
archivesBaseName = 'zcash-android-wallet'
|
||||
group = 'cash.z.ecc.android'
|
||||
version = '1.0.0-alpha05'
|
||||
version = '1.0.0-alpha10'
|
||||
|
||||
android {
|
||||
compileSdkVersion Deps.compileSdkVersion
|
||||
|
@ -19,12 +21,13 @@ android {
|
|||
applicationId 'cash.z.ecc.android'
|
||||
minSdkVersion Deps.minSdkVersion
|
||||
targetSdkVersion Deps.targetSdkVersion
|
||||
versionCode = 1_00_00_005
|
||||
versionCode = 1_00_00_010
|
||||
// last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
|
||||
versionName = "$version"
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
multiDexEnabled true
|
||||
// manifestPlaceholders = [rollbarToken: properties["rollbarToken"]]
|
||||
}
|
||||
flavorDimensions 'network'
|
||||
productFlavors {
|
||||
|
@ -87,6 +90,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
crashlytics {
|
||||
enableNdk true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':qrecycler')
|
||||
|
@ -134,7 +141,12 @@ dependencies {
|
|||
// 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
|
||||
implementation 'com.google.guava:guava:27.0.1-android'
|
||||
|
||||
// Analytics
|
||||
implementation 'com.mixpanel.android:mixpanel-android:5.6.3'
|
||||
implementation 'com.google.firebase:firebase-analytics:17.2.1'
|
||||
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'
|
||||
|
||||
// QR Scanning
|
||||
implementation 'com.google.firebase:firebase-ml-vision:24.0.1'
|
||||
|
@ -144,10 +156,16 @@ dependencies {
|
|||
implementation "androidx.camera:camera-extensions:1.0.0-alpha05"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-alpha02"
|
||||
|
||||
// Misc.
|
||||
implementation 'com.airbnb.android:lottie:3.1.0'
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
|
||||
// Tests
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation Deps.Test.JUNIT
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3'
|
||||
androidTestImplementation Deps.Test.Android.JUNIT
|
||||
androidTestImplementation Deps.Test.Android.ESPRESSO
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ZcashTheme">
|
||||
<activity android:name=".ui.MainActivity" android:screenOrientation="portrait">
|
||||
|
|
|
@ -7,27 +7,37 @@ import androidx.camera.camera2.Camera2Config
|
|||
import androidx.camera.core.CameraXConfig
|
||||
import cash.z.ecc.android.di.component.AppComponent
|
||||
import cash.z.ecc.android.di.component.DaggerAppComponent
|
||||
import cash.z.ecc.android.feedback.Feedback
|
||||
import cash.z.ecc.android.feedback.FeedbackCoordinator
|
||||
import cash.z.wallet.sdk.ext.SilentTwig
|
||||
import cash.z.wallet.sdk.ext.TroubleshootingTwig
|
||||
import cash.z.wallet.sdk.ext.Twig
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
||||
class ZcashWalletApp : Application(), CameraXConfig.Provider {
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
lateinit var coordinator: FeedbackCoordinator
|
||||
|
||||
var creationTime: Long = 0
|
||||
private set
|
||||
|
||||
var creationMeasured: Boolean = false
|
||||
|
||||
/**
|
||||
* Intentionally private Scope for use with launching Feedback jobs. The feedback object has the
|
||||
* longest scope in the app because it needs to be around early in order to measure launch times
|
||||
* and stick around late in order to catch crashes. We intentionally don't expose this because
|
||||
* application objects can have odd lifecycles, given that there is no clear onDestroy moment in
|
||||
* many cases.
|
||||
*/
|
||||
private var feedbackScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun onCreate() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
|
||||
Twig.plant(SilentTwig())
|
||||
creationTime = System.currentTimeMillis()
|
||||
instance = this
|
||||
// Setup handler for uncaught exceptions.
|
||||
|
@ -35,8 +45,9 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
|
|||
|
||||
component = DaggerAppComponent.factory().create(this)
|
||||
component.inject(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(feedbackCoordinator, Thread.getDefaultUncaughtExceptionHandler()))
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
feedbackScope.launch {
|
||||
coordinator.feedback.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
|
@ -58,17 +69,18 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
|
|||
* is complete, we can lazily initialize all the feedback objects at this moment so that we
|
||||
* don't have to add any time to startup.
|
||||
*/
|
||||
class ExceptionReporter(private val coordinator: FeedbackCoordinator, private val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
|
||||
inner class ExceptionReporter(private val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(t: Thread?, e: Throwable?) {
|
||||
twig("Uncaught Exception: $e")
|
||||
twig("Uncaught Exception: $e caused by: ${e?.cause}")
|
||||
coordinator.feedback.report(e)
|
||||
coordinator.flush()
|
||||
// can do this if necessary but first verify that we need it
|
||||
runBlocking {
|
||||
coordinator.await()
|
||||
coordinator.feedback.stop()
|
||||
coordinator.await()
|
||||
coordinator.feedback.stop()
|
||||
}
|
||||
ogHandler.uncaughtException(t, e)
|
||||
Thread.sleep(2000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import cash.z.ecc.android.di.annotation.ViewModelKey
|
|||
import cash.z.ecc.android.di.viewmodel.ViewModelFactory
|
||||
import cash.z.ecc.android.ui.detail.WalletDetailViewModel
|
||||
import cash.z.ecc.android.ui.home.HomeViewModel
|
||||
import cash.z.ecc.android.ui.profile.ProfileViewModel
|
||||
import cash.z.ecc.android.ui.receive.ReceiveViewModel
|
||||
import cash.z.ecc.android.ui.scan.ScanViewModel
|
||||
import cash.z.ecc.android.ui.send.SendViewModel
|
||||
|
@ -51,6 +52,12 @@ abstract class ViewModelsSynchronizerModule {
|
|||
@ViewModelKey(ScanViewModel::class)
|
||||
abstract fun bindScanViewModel(implementation: ScanViewModel): ViewModel
|
||||
|
||||
@SynchronizerScope
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ProfileViewModel::class)
|
||||
abstract fun bindProfileViewModel(implementation: ProfileViewModel): ViewModel
|
||||
|
||||
/**
|
||||
* Factory for view models that are not created until the Synchronizer exists. Only VMs that
|
||||
* require the Synchronizer should wait until it is created. In other words, these are the VMs
|
||||
|
|
|
@ -19,10 +19,20 @@ object Report {
|
|||
SEED_PHRASE_LOADED("metric.seedphrase.loaded", "seed phrase loaded"),
|
||||
WALLET_CREATED("metric.wallet.created", "wallet created"),
|
||||
WALLET_IMPORTED("metric.wallet.imported", "wallet imported"),
|
||||
ACCOUNT_CREATED("metric.account.created", "account created")
|
||||
ACCOUNT_CREATED("metric.account.created", "account created"),
|
||||
|
||||
// Transactions
|
||||
TRANSACTION_INITIALIZED("metric.tx.initialized", "transaction initialized"),
|
||||
TRANSACTION_CREATED("metric.tx.created", "transaction created successfully"),
|
||||
TRANSACTION_SUBMITTED("metric.tx.submitted", "transaction submitted successfully"),
|
||||
TRANSACTION_MINED("metric.tx.mined", "transaction mined")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a metric with a start time of ZcashWalletApp.creationTime and an end time of when this
|
||||
* instance was created. This can then be passed to [Feedback.report].
|
||||
*/
|
||||
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
|
||||
Feedback.Metric by metric {
|
||||
constructor() : this(
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package cash.z.ecc.android.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
|
@ -15,6 +18,7 @@ import android.view.inputmethod.InputMethodManager
|
|||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -46,16 +50,18 @@ class MainActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var clipboard: ClipboardManager
|
||||
|
||||
|
||||
private val mediaPlayer: MediaPlayer = MediaPlayer()
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
|
||||
lateinit var navController: NavController
|
||||
|
||||
lateinit var component: MainActivitySubcomponent
|
||||
lateinit var synchronizerComponent: SynchronizerSubcomponent
|
||||
|
||||
private val hasCameraPermission
|
||||
get() = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component = ZcashWalletApp.component.mainActivitySubcomponent().create(this).also {
|
||||
|
@ -163,6 +169,13 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
fun copyText(textToCopy: String, label: String = "zECC Wallet Text") {
|
||||
clipboard.setPrimaryClip(
|
||||
ClipData.newPlainText(label, textToCopy)
|
||||
)
|
||||
showMessage("$label copied!", "Sweet")
|
||||
}
|
||||
|
||||
fun preventBackPress(fragment: Fragment) {
|
||||
onFragmentBackPressed(fragment){}
|
||||
}
|
||||
|
@ -204,4 +217,43 @@ class MainActivity : AppCompatActivity() {
|
|||
if (!it.isShownOrQueued) it.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
fun maybeOpenScan(popUpToInclusive: Int? = null) {
|
||||
if (hasCameraPermission) {
|
||||
openCamera(popUpToInclusive)
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), 101)
|
||||
} else {
|
||||
onNoCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == 101) {
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
openCamera()
|
||||
} else {
|
||||
onNoCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCamera(popUpToInclusive: Int? = null) {
|
||||
navController.navigate(popUpToInclusive ?: R.id.action_global_nav_scan)
|
||||
}
|
||||
|
||||
private fun onNoCamera() {
|
||||
showSnackbar("Well, this is awkward. You denied permission for the camera.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@ class TransactionAdapter<T : ConfirmedTransaction> :
|
|||
override fun areItemsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem.minedHeight == newItem.minedHeight
|
||||
) = oldItem.minedHeight == newItem.minedHeight && oldItem.noteId == newItem.noteId
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem.equals(newItem)
|
||||
) = oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
|
@ -33,5 +33,4 @@ class TransactionAdapter<T : ConfirmedTransaction> :
|
|||
holder: TransactionViewHolder<T>,
|
||||
position: Int
|
||||
) = holder.bindTo(getItem(position))
|
||||
|
||||
}
|
||||
|
|
|
@ -4,10 +4,14 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.R
|
||||
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.toAbbreviatedAddress
|
||||
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.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
@ -16,16 +20,22 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
|
|||
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||
private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top)
|
||||
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
|
||||
private val shieldIcon = itemView.findViewById<View>(R.id.image_shield)
|
||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||
|
||||
fun bindTo(transaction: T?) {
|
||||
|
||||
// update view
|
||||
var lineOne: String = ""
|
||||
var lineTwo: String = ""
|
||||
var amount: String = ""
|
||||
var amountColor: Int = 0
|
||||
var indicatorBackground: Int = 0
|
||||
|
||||
|
||||
transaction?.apply {
|
||||
itemView.setOnClickListener {
|
||||
onTransactionClicked(this)
|
||||
}
|
||||
amount = value.convertZatoshiToZecString()
|
||||
// TODO: these might be good extension functions
|
||||
val timestamp = formatter.format(blockTimeInSeconds * 1000L)
|
||||
|
@ -58,5 +68,33 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
|
|||
amountText.setTextColor(amountColor.toAppColor())
|
||||
val context = itemView.context
|
||||
indicator.background = context.resources.getDrawable(indicatorBackground)
|
||||
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTransactionClicked(transaction: ConfirmedTransaction) {
|
||||
val txId = transaction.rawTransactionId.toTxId()
|
||||
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
|
||||
"Transaction: $txId"
|
||||
MaterialAlertDialogBuilder(itemView.context)
|
||||
.setMessage(detailsMessage)
|
||||
.setTitle("Transaction Details")
|
||||
.setCancelable(true)
|
||||
.setPositiveButton("Ok") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Copy TX") { dialog, _ ->
|
||||
(itemView.context as MainActivity).copyText(txId, "Transaction Id")
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toTxId(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
for(i in (size - 1) downTo 0) {
|
||||
sb.append(String.format("%02x", this[i]))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package cash.z.ecc.android.ui.detail
|
||||
//
|
||||
//import android.content.Context
|
||||
//import android.graphics.Canvas
|
||||
//import android.graphics.Rect
|
||||
//import android.view.LayoutInflater
|
||||
//import android.view.View
|
||||
//import androidx.recyclerview.widget.RecyclerView
|
||||
//import cash.z.ecc.android.R
|
||||
//
|
||||
//
|
||||
//class TransactionsDrawableFooter(context: Context) : RecyclerView.ItemDecoration() {
|
||||
//
|
||||
// private var footer: View =
|
||||
// LayoutInflater.from(context).inflate(R.layout.footer_transactions, null, false)
|
||||
//
|
||||
// override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
// super.onDraw(c, parent, state!!)
|
||||
// footer.measure(
|
||||
// View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.AT_MOST),
|
||||
// View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
// )
|
||||
// // layout basically just gets drawn on the reserved space on top of the first view
|
||||
// footer.layout(parent.left, 0, parent.right, footer.measuredHeight)
|
||||
// for (i in 0 until parent.childCount) {
|
||||
// val view: View = parent.getChildAt(i)
|
||||
// if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
|
||||
// c.save()
|
||||
// val height: Int = footer.measuredHeight
|
||||
// val top: Int = view.top - height
|
||||
// c.translate(0.0f, top.toFloat())
|
||||
// footer.draw(c)
|
||||
// c.restore()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun getItemOffsets(
|
||||
// outRect: Rect,
|
||||
// view: View,
|
||||
// parent: RecyclerView,
|
||||
// state: RecyclerView.State
|
||||
// ) {
|
||||
// super.getItemOffsets(outRect, view, parent, state)
|
||||
// if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
|
||||
// outRect.set(0, 0, 0, 150)
|
||||
// } else {
|
||||
// outRect.setEmpty()
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,49 @@
|
|||
package cash.z.ecc.android.ui.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.R
|
||||
|
||||
|
||||
class TransactionsFooter(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private var footer: Drawable = context.resources.getDrawable(R.drawable.background_footer)
|
||||
val bounds = Rect()
|
||||
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
c.save()
|
||||
val left: Int = 0
|
||||
val right: Int = parent.width
|
||||
val childCount = parent.childCount
|
||||
val adapterItemCount = parent.adapter!!.itemCount
|
||||
for (i in 0 until childCount) {
|
||||
val child = parent.getChildAt(i)
|
||||
if (parent.getChildAdapterPosition(child) == adapterItemCount - 1) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
val bottom: Int = bounds.bottom + Math.round(child.translationY)
|
||||
val top: Int = bottom - footer.intrinsicHeight
|
||||
footer.setBounds(left, top, right, bottom)
|
||||
footer.draw(c)
|
||||
}
|
||||
}
|
||||
c.restore()
|
||||
}
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
if (parent.getChildAdapterPosition(view) == parent.adapter!!.itemCount - 1) {
|
||||
outRect.set(0, 0, 0, footer.intrinsicHeight)
|
||||
} else {
|
||||
outRect.setEmpty()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,16 +49,18 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
|
|||
|
||||
private fun onBalanceUpdated(balance: WalletBalance) {
|
||||
binding.textBalanceAvailable.text = balance.availableZatoshi.convertZatoshiToZecString()
|
||||
val change = balance.totalZatoshi - balance.availableZatoshi
|
||||
val change = (balance.totalZatoshi - balance.availableZatoshi)
|
||||
binding.textBalanceDescription.apply {
|
||||
goneIf(change <= 0)
|
||||
text = "(expecting +$change ZEC in change)".toColoredSpan(R.color.text_light, "+$change")
|
||||
goneIf(change <= 0L)
|
||||
val changeString = change.convertZatoshiToZecString()
|
||||
text = "(expecting +$changeString ZEC)".toColoredSpan(R.color.text_light, "+${changeString}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTransactionUI() {
|
||||
binding.recyclerTransactions.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
binding.recyclerTransactions.addItemDecoration(TransactionsFooter(binding.recyclerTransactions.context))
|
||||
adapter = TransactionAdapter()
|
||||
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
|
||||
binding.recyclerTransactions.adapter = adapter
|
||||
|
@ -66,6 +68,12 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
|
|||
|
||||
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
|
||||
twig("got a new paged list of transactions")
|
||||
binding.groupEmptyViews.goneIf(transactions.size > 0)
|
||||
adapter.submitList(transactions)
|
||||
}
|
||||
|
||||
// TODO: maybe implement this for better fade behavior. Or do an actual scroll behavior instead, yeah do that. Or an item decoration.
|
||||
fun onLastItemShown(item: ConfirmedTransaction, position: Int) {
|
||||
binding.footerFade.alpha = position.toFloat() / (binding.recyclerTransactions.adapter?.itemCount ?: 1)
|
||||
}
|
||||
}
|
|
@ -2,32 +2,30 @@ 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.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.toAppColor
|
||||
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.Status.SYNCING
|
||||
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -41,8 +39,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
private val sendViewModel: SendViewModel by activityViewModel()
|
||||
private val viewModel: HomeViewModel by viewModel()
|
||||
|
||||
private val _typedChars = ConflatedBroadcastChannel<Char>()
|
||||
private val typedChars = _typedChars.asFlow()
|
||||
lateinit var snake: MagicSnakeLoader
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
|
||||
FragmentHomeBinding.inflate(inflater)
|
||||
|
@ -54,6 +51,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
|
||||
override fun onAttach(context: Context) {
|
||||
twig("HomeFragment.onAttach")
|
||||
twig("ZZZ")
|
||||
twig("ZZZ")
|
||||
twig("ZZZ")
|
||||
twig("ZZZ ===================== HOME FRAGMENT CREATED ==================================")
|
||||
super.onAttach(context)
|
||||
|
||||
// this will call startSync either now or later (after initializing with newly created seed)
|
||||
|
@ -92,15 +93,19 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile)
|
||||
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
|
||||
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail)
|
||||
hitAreaScan.onClickNavTo(R.id.action_nav_home_to_nav_scan)
|
||||
hitAreaScan.setOnClickListener {
|
||||
mainActivity?.maybeOpenScan()
|
||||
}
|
||||
|
||||
textBannerAction.setOnClickListener {
|
||||
onBannerAction(BannerAction.from((it as? TextView)?.text?.toString()))
|
||||
}
|
||||
buttonSend.setOnClickListener {
|
||||
buttonSendAmount.setOnClickListener {
|
||||
onSend()
|
||||
}
|
||||
setSendAmount("0")
|
||||
setSendAmount("0", false)
|
||||
|
||||
snake = MagicSnakeLoader(binding.lottieButtonLoading)
|
||||
}
|
||||
|
||||
binding.buttonNumberPadBack.setOnLongClickListener {
|
||||
|
@ -108,46 +113,60 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
true
|
||||
}
|
||||
|
||||
// if (::uiModel.isInitialized) {
|
||||
// twig("uiModel exists!")
|
||||
// onModelUpdated(HomeViewModel.UiModel(), uiModel)
|
||||
// }
|
||||
if (::uiModel.isInitialized) {
|
||||
twig("uiModel exists!")
|
||||
onModelUpdated(null, uiModel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClearAmount() {
|
||||
repeat(binding.textSendAmount.text.length) {
|
||||
if (::uiModel.isInitialized) {
|
||||
resumedScope.launch {
|
||||
_typedChars.send('<')
|
||||
binding.textSendAmount.text.apply {
|
||||
while (uiModel.pendingSend != "0") {
|
||||
viewModel.onChar('<')
|
||||
delay(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.initialize(typedChars)
|
||||
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
|
||||
}.onCompletion {
|
||||
twig("uiModel.scanReduce completed.")
|
||||
}.catch { e ->
|
||||
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) {
|
||||
super.onSaveInstanceState(outState)
|
||||
twig("HomeFragment.onSaveInstanceState")
|
||||
if (::uiModel.isInitialized) {
|
||||
outState.putParcelable("uiModel", uiModel)
|
||||
// outState.putParcelable("uiModel", uiModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +174,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
super.onViewStateRestored(savedInstanceState)
|
||||
savedInstanceState?.let { inState ->
|
||||
twig("HomeFragment.onViewStateRestored")
|
||||
onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
|
||||
// onModelUpdated(HomeViewModel.UiModel(), inState.getParcelable("uiModel")!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,29 +184,64 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
//
|
||||
|
||||
fun setSendEnabled(enabled: Boolean) {
|
||||
binding.buttonSend.apply {
|
||||
binding.buttonSendAmount.apply {
|
||||
isEnabled = enabled
|
||||
backgroundTintList = ColorStateList.valueOf( resources.getColor( if(enabled) R.color.colorPrimary else R.color.zcashWhite_24) )
|
||||
if (enabled) {
|
||||
// setTextColor(resources.getColorStateList(R.color.selector_button_text_dark))
|
||||
binding.lottieButtonLoading.alpha = 1.0f
|
||||
} else {
|
||||
// setTextColor(R.color.zcashGray.toAppColor())
|
||||
binding.lottieButtonLoading.alpha = 0.32f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgress(progress: Int) {
|
||||
progress.let {
|
||||
if (it < 100) {
|
||||
setBanner("Downloading . . . $it%", NONE)
|
||||
} else {
|
||||
setBanner("Scanning . . .", NONE)
|
||||
}
|
||||
fun setProgress(uiModel: HomeViewModel.UiModel) {
|
||||
if (!uiModel.processorInfo.hasData) {
|
||||
twig("Warning: ignoring progress update because the processor has not started.")
|
||||
return
|
||||
}
|
||||
|
||||
snake.isSynced = uiModel.isSynced
|
||||
if (!uiModel.isSynced) {
|
||||
snake.downloadProgress = uiModel.downloadProgress
|
||||
snake.scanProgress = uiModel.scanProgress
|
||||
}
|
||||
|
||||
val sendText = when {
|
||||
uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE"
|
||||
uiModel.status == Synchronizer.Status.DISCONNECTED -> "DISCONNECTED"
|
||||
uiModel.status == Synchronizer.Status.STOPPED -> "IDLE"
|
||||
uiModel.isDownloading -> "Downloading . . . ${snake.downloadProgress}%"
|
||||
uiModel.isValidating -> "Validating . . ."
|
||||
uiModel.isScanning -> "Scanning . . . ${snake.scanProgress}%"
|
||||
else -> "Updating"
|
||||
}
|
||||
|
||||
// binding.lottieButtonLoading.progress = if (uiModel.isSynced) 1.0f else uiModel.totalProgress * 0.82f // line fully closes at 82% mark
|
||||
binding.buttonSendAmount.text = sendText
|
||||
// twig("Lottie progress set to ${binding.lottieButtonLoading.progress} (isSynced? ${uiModel.isSynced})")
|
||||
twig("Send button set to: $sendText")
|
||||
|
||||
val resId = if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
|
||||
binding.buttonSendAmount.setTextColor(resources.getColorStateList(resId))
|
||||
|
||||
// if (uiModel.status == DISCONNECTED || uiModel.status == STOPPED) {
|
||||
// binding.buttonSendAmount.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.zcashGray))
|
||||
// } else {
|
||||
// binding.buttonSendAmount.backgroundTintList = null
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param amount the amount to send represented as ZEC, without the dollar sign.
|
||||
*/
|
||||
fun setSendAmount(amount: String) {
|
||||
fun setSendAmount(amount: String, updateModel: Boolean = true) {
|
||||
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
|
||||
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
|
||||
binding.buttonSend.disabledIf(amount == "0")
|
||||
if (updateModel) {
|
||||
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
|
||||
}
|
||||
binding.buttonSendAmount.disabledIf(amount == "0")
|
||||
}
|
||||
|
||||
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) {
|
||||
|
@ -197,17 +251,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
goneIf(availableBalance < 0)
|
||||
text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
|
||||
val change = (totalBalance - availableBalance).convertZatoshiToZecString()
|
||||
"(expecting +$change ZEC in change)".toColoredSpan(R.color.text_light, "+$change")
|
||||
"(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
|
||||
} else {
|
||||
"(enter an amount to send)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSendText(buttonText: String = "Send Amount") {
|
||||
binding.buttonSend.text = buttonText
|
||||
}
|
||||
|
||||
fun setBanner(message: String = "", action: BannerAction = CLEAR) {
|
||||
with(binding) {
|
||||
val hasMessage = !message.isEmpty() || action != CLEAR
|
||||
|
@ -225,30 +275,38 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
// Private UI Events
|
||||
//
|
||||
|
||||
private fun onModelUpdated(old: HomeViewModel.UiModel, new: HomeViewModel.UiModel) {
|
||||
twig(new.toString())
|
||||
private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
|
||||
twig("onModelUpdated: $new")
|
||||
uiModel = new
|
||||
if (old.pendingSend != new.pendingSend) {
|
||||
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
|
||||
if (new.status == SYNCING) onSyncing(new) else onSynced(new)
|
||||
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) {
|
||||
setProgress(uiModel.progress) // calls setBanner
|
||||
setAvailable()
|
||||
setSendText("Syncing Blockchain…")
|
||||
}
|
||||
|
||||
private fun onSynced(uiModel: HomeViewModel.UiModel) {
|
||||
snake.isSynced = true
|
||||
if (!uiModel.hasBalance) {
|
||||
onNoFunds()
|
||||
} else {
|
||||
setBanner("")
|
||||
setAvailable(uiModel.availableBalance, uiModel.totalBalance)
|
||||
setSendText()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,18 +318,27 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
when (action) {
|
||||
FUND_NOW -> {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setMessage("To make full use of this wallet, deposit funds to your address or tap the faucet to trigger a tiny automatic deposit.\n\nFaucet funds are made available for the community by the community for testing. So please be kind enough to return what you borrow!")
|
||||
.setMessage("To make full use of this wallet, deposit funds to your address.")
|
||||
.setTitle("No Balance")
|
||||
.setCancelable(true)
|
||||
.setPositiveButton("Tap Faucet") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
setBanner("Tapping faucet...", CANCEL)
|
||||
}
|
||||
.setNegativeButton("View Address") { dialog, _ ->
|
||||
.setPositiveButton("View Address") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_home_to_nav_receive)
|
||||
}
|
||||
.show()
|
||||
// MaterialAlertDialogBuilder(activity)
|
||||
// .setMessage("To make full use of this wallet, deposit funds to your address or tap the faucet to trigger a tiny automatic deposit.\n\nFaucet funds are made available for the community by the community for testing. So please be kind enough to return what you borrow!")
|
||||
// .setTitle("No Balance")
|
||||
// .setCancelable(true)
|
||||
// .setPositiveButton("Tap Faucet") { dialog, _ ->
|
||||
// dialog.dismiss()
|
||||
// setBanner("Tapping faucet...", CANCEL)
|
||||
// }
|
||||
// .setNegativeButton("View Address") { dialog, _ ->
|
||||
// dialog.dismiss()
|
||||
// mainActivity?.navController?.navigate(R.id.action_nav_home_to_nav_receive)
|
||||
// }
|
||||
// .show()
|
||||
}
|
||||
CANCEL -> {
|
||||
// TODO: trigger banner / balance update
|
||||
|
@ -310,7 +377,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
twig("CHAR TYPED: $c")
|
||||
_typedChars.send(c)
|
||||
viewModel.onChar(c)
|
||||
}
|
||||
}
|
||||
return this
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.wallet.sdk.SdkSynchronizer
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.DISCONNECTED
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.*
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.scan
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class HomeViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
|
@ -23,11 +20,12 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|
|||
|
||||
lateinit var uiModels: Flow<UiModel>
|
||||
|
||||
private val _typedChars = ConflatedBroadcastChannel<Char>()
|
||||
private val typedChars = _typedChars.asFlow()
|
||||
|
||||
var initialized = false
|
||||
|
||||
fun initialize(
|
||||
typedChars: Flow<Char>
|
||||
) {
|
||||
fun initializeMaybe() {
|
||||
twig("init called")
|
||||
if (initialized) {
|
||||
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
|
||||
|
@ -55,8 +53,9 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
twig("initializing view models stream")
|
||||
uiModels = synchronizer.run {
|
||||
combine(status, progress, balances, zec) { s, p, b, z->
|
||||
combine(status, processorInfo, balances, zec) { s, p, b, z->
|
||||
UiModel(s, p, b.availableZatoshi, b.totalZatoshi, z)
|
||||
}
|
||||
}.conflate()
|
||||
|
@ -67,22 +66,64 @@ class HomeViewModel @Inject constructor() : ViewModel() {
|
|||
twig("HomeViewModel cleared!")
|
||||
}
|
||||
|
||||
suspend fun onChar(c: Char) {
|
||||
_typedChars.send(c)
|
||||
}
|
||||
|
||||
suspend fun refreshBalance() {
|
||||
(synchronizer as SdkSynchronizer).refreshBalance()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
|
||||
val status: Synchronizer.Status = DISCONNECTED,
|
||||
val progress: Int = 0,
|
||||
val processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
|
||||
val availableBalance: Long = -1L,
|
||||
val totalBalance: Long = -1L,
|
||||
val pendingSend: String = "0"
|
||||
): Parcelable {
|
||||
) {
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasFunds: Boolean get() = availableBalance > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.0001
|
||||
val hasBalance: Boolean get() = totalBalance > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.0001
|
||||
val hasBalance: Boolean get() = totalBalance > 0
|
||||
val isSynced: Boolean get() = status == SYNCED
|
||||
val isSendEnabled: Boolean get() = isSynced && hasFunds
|
||||
|
||||
// Processor Info
|
||||
val isDownloading = status == DOWNLOADING
|
||||
val isScanning = status == SCANNING
|
||||
val isValidating = status == VALIDATING
|
||||
val downloadProgress: Int get() {
|
||||
return processorInfo.run {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val scanProgress: Int get() {
|
||||
return processorInfo.run {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val totalProgress: Float get() {
|
||||
val downloadWeighted = 0.40f * (downloadProgress.toFloat() / 100.0f).coerceAtMost(1.0f)
|
||||
val scanWeighted = 0.60f * (scanProgress.toFloat() / 100.0f).coerceAtMost(1.0f)
|
||||
return downloadWeighted.coerceAtLeast(0.0f) + scanWeighted.coerceAtLeast(0.0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
|
||||
class MagicSnakeLoader(
|
||||
val lottie: LottieAnimationView,
|
||||
private val scanningStartFrame: Int = 100,
|
||||
private val scanningEndFrame: Int = 175,
|
||||
val totalFrames: Int = 200
|
||||
) : ValueAnimator.AnimatorUpdateListener {
|
||||
private var isPaused: Boolean = true
|
||||
private var isStarted: Boolean = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
var downloadProgress: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
twig("ZZZ downloadProgress=$value")
|
||||
if (value > 0) startMaybe()
|
||||
}
|
||||
|
||||
private fun startMaybe() {
|
||||
|
||||
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???") }
|
||||
}
|
||||
// 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
|
||||
|
||||
init {
|
||||
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
|
||||
animation.currentFrame().let { frame ->
|
||||
if (isDownloading) allowLoop(frame) else applyScanProgress(frame)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
// once we're ready to show scan progress, do it! Don't do extra loops.
|
||||
if (frame >= scanningStartFrame || frame in acceptablePauseFrames) {
|
||||
twig("ZZZ pausing so we can scan! ${if(frame<scanningStartFrame) "WE STOPPED EARLY!" else ""}")
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanUpdated() {
|
||||
twig("ZZZ onScanUpdated : isPaused=$isPaused")
|
||||
if (isSynced) {
|
||||
// playToCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
if (isPaused && isStarted) {
|
||||
// move forward within the scan range, proportionate to how much scanning is complete
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun playToCompletion() {
|
||||
removeLoops()
|
||||
twig("ZZZ playing to completion")
|
||||
unpause()
|
||||
}
|
||||
|
||||
private fun removeLoops() {
|
||||
lottie.frame.let {frame ->
|
||||
if (frame in 33..67) {
|
||||
twig("ZZZ removing 1 loop!")
|
||||
lottie.frame = frame + 34
|
||||
} else if (frame in 0..33) {
|
||||
twig("ZZZ removing 2 loops!")
|
||||
lottie.frame = frame + 67
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
if (!isPaused) {
|
||||
twig("ZZZ pausing")
|
||||
lottie.pauseAnimation()
|
||||
isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValueAnimator.currentFrame(): Int {
|
||||
return ((animatedValue as Float) * totalFrames).toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,20 @@ import android.view.View
|
|||
import cash.z.ecc.android.BuildConfig
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentProfileBinding
|
||||
import cash.z.ecc.android.di.viewmodel.viewModel
|
||||
import cash.z.ecc.android.ext.onClick
|
||||
import cash.z.ecc.android.ext.onClickNavBack
|
||||
import cash.z.ecc.android.ext.onClickNavTo
|
||||
import cash.z.ecc.android.feedback.FeedbackFile
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.Okio
|
||||
|
||||
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
|
||||
|
||||
private val viewModel: ProfileViewModel by viewModel()
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentProfileBinding =
|
||||
FragmentProfileBinding.inflate(inflater)
|
||||
|
||||
|
@ -31,6 +37,13 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumedScope.launch {
|
||||
binding.textAddress.text = viewModel.getAddress().toAbbreviatedAddress(12, 12)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onViewLogs() {
|
||||
loadLogFileAsText().let { logText ->
|
||||
if (logText == null) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package cash.z.ecc.android.ui.profile
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProfileViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var synchronizer: Synchronizer
|
||||
|
||||
suspend fun getAddress(): String = synchronizer.getAddress()
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
twig("ProfileViewModel cleared!")
|
||||
}
|
||||
}
|
|
@ -39,7 +39,9 @@ class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() {
|
|||
// text_address_part_7,
|
||||
// text_address_part_8
|
||||
// )
|
||||
binding.buttonScan.onClickNavTo(R.id.action_nav_receive_to_nav_scan)
|
||||
binding.buttonScan.setOnClickListener {
|
||||
mainActivity?.maybeOpenScan(R.id.action_nav_receive_to_nav_scan)
|
||||
}
|
||||
binding.backButtonHitArea.onClickNavBack()
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,6 @@ class ReceiveViewModel @Inject constructor() : ViewModel() {
|
|||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
twig("WalletDetailViewModel cleared!")
|
||||
twig("ReceiveViewModel cleared!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,20 @@ package cash.z.ecc.android.ui.send
|
|||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentSendAddressBinding
|
||||
import cash.z.ecc.android.di.viewmodel.activityViewModel
|
||||
import cash.z.ecc.android.ext.*
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance
|
||||
import cash.z.wallet.sdk.ext.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -18,6 +23,8 @@ import kotlinx.coroutines.launch
|
|||
class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
||||
ClipboardManager.OnPrimaryClipChangedListener {
|
||||
|
||||
private var maxZatoshi: Long? = null
|
||||
|
||||
val sendViewModel: SendViewModel by activityViewModel()
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentSendAddressBinding =
|
||||
|
@ -25,55 +32,97 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButtonHitArea.onClickNavTo(R.id.action_nav_send_address_to_nav_home)
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onSubmit()
|
||||
}
|
||||
binding.backButtonHitArea.onClickNavTo(R.id.action_nav_send_address_to_nav_home)
|
||||
binding.textBannerAction.setOnClickListener {
|
||||
onPaste()
|
||||
}
|
||||
binding.textBannerMessage.setOnClickListener {
|
||||
onPaste()
|
||||
}
|
||||
binding.textMax.setOnClickListener {
|
||||
onMax()
|
||||
}
|
||||
|
||||
// Apply View Model
|
||||
if (sendViewModel.zatoshiAmount > 0L) {
|
||||
sendViewModel.zatoshiAmount.convertZatoshiToZecString(8).let { amount ->
|
||||
binding.inputZcashAmount.setText(amount)
|
||||
binding.textAmount.text = "Sending $amount ZEC"
|
||||
}
|
||||
} else {
|
||||
binding.inputZcashAmount.setText(null)
|
||||
}
|
||||
if (!sendViewModel.toAddress.isNullOrEmpty()){
|
||||
binding.textAmount.text = "Send to ${sendViewModel.toAddress.toAbbreviatedAddress()}"
|
||||
if (!sendViewModel.toAddress.isNullOrEmpty()) {
|
||||
binding.inputZcashAddress.setText(sendViewModel.toAddress)
|
||||
} else {
|
||||
binding.inputZcashAddress.setText(null)
|
||||
}
|
||||
|
||||
binding.inputZcashAddress.onEditorActionDone(::onSubmit)
|
||||
binding.inputZcashAmount.onEditorActionDone(::onSubmit)
|
||||
|
||||
binding.imageScanQr.onClickNavTo(R.id.action_nav_send_address_to_nav_scan)
|
||||
binding.inputZcashAddress.apply {
|
||||
doAfterTextChanged {
|
||||
val trim = text.toString().trim()
|
||||
if (text.toString() != trim) {
|
||||
binding.inputZcashAddress
|
||||
.findViewById<EditText>(R.id.input_zcash_address).setText(trim)
|
||||
}
|
||||
onAddressChanged(trim)
|
||||
}
|
||||
}
|
||||
|
||||
binding.textLayoutAddress.setEndIconOnClickListener {
|
||||
mainActivity?.maybeOpenScan()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAddressChanged(address: String) {
|
||||
resumedScope.launch {
|
||||
var type = when (sendViewModel.validateAddress(address)) {
|
||||
is Synchronizer.AddressType.Transparent -> "This is a valid transparent address" to R.color.zcashGreen
|
||||
is Synchronizer.AddressType.Shielded -> "This is a valid shielded address" to R.color.zcashGreen
|
||||
is Synchronizer.AddressType.Invalid -> "This address appears to be invalid" to R.color.zcashRed
|
||||
}
|
||||
if (address == sendViewModel.synchronizer.getAddress()) type =
|
||||
"Warning, this appears to be your address!" to R.color.zcashRed
|
||||
binding.textLayoutAddress.helperText = type.first
|
||||
binding.textLayoutAddress.setHelperTextColor(ColorStateList.valueOf(type.second.toAppColor()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun onSubmit(unused: EditText? = null) {
|
||||
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
|
||||
binding.inputZcashAmount.convertZecToZatoshi()?.let { sendViewModel.zatoshiAmount = it }
|
||||
sendViewModel.validate().onFirstWith(resumedScope) {
|
||||
sendViewModel.validate(maxZatoshi).onFirstWith(resumedScope) {
|
||||
if (it == null) {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_address_to_send_memo)
|
||||
} else {
|
||||
resumedScope.launch {
|
||||
binding.textAddressError.text = it
|
||||
delay(1500L)
|
||||
binding.textAddressError.text = ""
|
||||
binding.textAddressError.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMax() {
|
||||
if (maxZatoshi != null) {
|
||||
binding.inputZcashAmount.apply {
|
||||
setText(maxZatoshi.convertZatoshiToZecString(8))
|
||||
postDelayed({
|
||||
requestFocus()
|
||||
setSelection(text?.length ?: 0)
|
||||
}, 10L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
|
||||
|
@ -87,6 +136,18 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateClipboardBanner()
|
||||
sendViewModel.synchronizer.balances.collectWith(resumedScope) {
|
||||
onBalanceUpdated(it)
|
||||
}
|
||||
binding.inputZcashAddress.text.toString().let {
|
||||
if (!it.isNullOrEmpty()) onAddressChanged(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBalanceUpdated(balance: WalletBalance) {
|
||||
binding.textLayoutAmount.helperText =
|
||||
"You have ${balance.availableZatoshi.convertZatoshiToZecString(8)} available"
|
||||
maxZatoshi = balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
|
|
|
@ -9,11 +9,15 @@ import cash.z.ecc.android.R
|
|||
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
|
||||
import cash.z.ecc.android.di.viewmodel.activityViewModel
|
||||
import cash.z.ecc.android.ext.goneIf
|
||||
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.ui.base.BaseFragment
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
@ -32,6 +36,9 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
|
|||
binding.buttonNext.setOnClickListener {
|
||||
onExit()
|
||||
}
|
||||
binding.buttonRetry.setOnClickListener {
|
||||
onRetry()
|
||||
}
|
||||
binding.backButtonHitArea.setOnClickListener {
|
||||
onExit()
|
||||
}
|
||||
|
@ -69,28 +76,47 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
|
|||
}
|
||||
|
||||
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
|
||||
val id = pendingTransaction?.id ?: -1
|
||||
var isSending = true
|
||||
val message = when {
|
||||
pendingTransaction == null -> "Transaction not found"
|
||||
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation . . ."
|
||||
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
|
||||
pendingTransaction.isCreating() -> "Creating transaction . . ."
|
||||
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
|
||||
try {
|
||||
if (pendingTransaction != null) sendViewModel.updateMetrics(pendingTransaction)
|
||||
val id = pendingTransaction?.id ?: -1
|
||||
var isSending = true
|
||||
var isFailure = false
|
||||
val message = when {
|
||||
pendingTransaction == null -> "Transaction not found"
|
||||
pendingTransaction.isMined() -> "Transaction Mined!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation . . ."
|
||||
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false; isFailure = true }
|
||||
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false; isFailure = true }
|
||||
pendingTransaction.isCreated() -> "Transaction creation complete!"
|
||||
pendingTransaction.isCreating() -> "Creating transaction . . ."
|
||||
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
|
||||
}
|
||||
twig("Pending TX (id: ${pendingTransaction?.id} Updated with message: $message")
|
||||
binding.textStatus.apply {
|
||||
text = "$message"
|
||||
}
|
||||
binding.backButton.goneIf(!binding.textStatus.text.toString().contains("Awaiting"))
|
||||
binding.buttonNext.goneIf((pendingTransaction?.isSubmitSuccess() != true) && (pendingTransaction?.isCreated() != true) && !isFailure)
|
||||
binding.buttonNext.text = if (isSending) "Done" else "Finished"
|
||||
binding.buttonRetry.goneIf(!isFailure)
|
||||
binding.progressHorizontal.goneIf(!isSending)
|
||||
|
||||
|
||||
if (pendingTransaction?.isSubmitSuccess() == true) {
|
||||
sendViewModel.reset()
|
||||
}
|
||||
} catch(t: Throwable) {
|
||||
twig("ERROR: error while handling pending transaction update! $t")
|
||||
Crashlytics.logException(t)
|
||||
}
|
||||
twig("Pending TX Updated: $message")
|
||||
binding.textStatus.apply {
|
||||
text = "$text\n$message"
|
||||
}
|
||||
binding.backButton.goneIf(!binding.textStatus.text.toString().contains("Awaiting"))
|
||||
binding.buttonNext.goneIf(isSending)
|
||||
binding.progressHorizontal.goneIf(!isSending)
|
||||
}
|
||||
|
||||
private fun onExit() {
|
||||
mainActivity?.navController?.popBackStack(R.id.nav_send_address, true)
|
||||
mainActivity?.navController?.popBackStack(R.id.nav_home, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
mainActivity?.navController?.popBackStack(R.id.nav_send_address, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ 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.ui.base.BaseFragment
|
||||
|
||||
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
||||
|
@ -22,10 +23,10 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonNext.setOnClickListener {
|
||||
onAddMemo()
|
||||
onTopButton()
|
||||
}
|
||||
binding.buttonSkip.setOnClickListener {
|
||||
onSkip()
|
||||
onBottomButton()
|
||||
}
|
||||
R.id.action_nav_send_memo_to_nav_send_address.let {
|
||||
binding.backButtonHitArea.onClickNavTo(it)
|
||||
|
@ -38,7 +39,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
|||
|
||||
binding.inputMemo.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onAddMemo()
|
||||
onTopButton()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -54,8 +55,20 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
|||
}
|
||||
|
||||
private fun applyModel() {
|
||||
binding.inputMemo.setText(sendViewModel.memo)
|
||||
binding.checkIncludeAddress.isChecked = sendViewModel.includeFromAddress
|
||||
sendViewModel.isShielded.let { isShielded ->
|
||||
binding.groupShielded.goneIf(!isShielded)
|
||||
binding.groupTransparent.goneIf(isShielded)
|
||||
if (isShielded) {
|
||||
binding.inputMemo.setText(sendViewModel.memo)
|
||||
binding.checkIncludeAddress.isChecked = sendViewModel.includeFromAddress
|
||||
binding.buttonNext.text = "ADD MEMO"
|
||||
binding.buttonSkip.text = "SEND WITHOUT MEMO"
|
||||
} else {
|
||||
binding.buttonNext.text = "WAIT, GO BACK"
|
||||
binding.buttonSkip.text = "PROCEED"
|
||||
binding.sadTitle.text = binding.sadTitle.text.toString().toColoredSpan(R.color.colorPrimary, "sad")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIncludeMemo(checked: Boolean) {
|
||||
|
@ -64,18 +77,22 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
|
|||
if (checked) binding.inputMemo.setHint("") else binding.inputMemo.setHint("Add a memo here")
|
||||
}
|
||||
|
||||
private fun onSkip() {
|
||||
private fun onTopButton() {
|
||||
if (sendViewModel.isShielded) {
|
||||
sendViewModel.memo = binding.inputMemo.text.toString()
|
||||
onNext()
|
||||
} else {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_nav_send_address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBottomButton() {
|
||||
binding.inputMemo.setText("")
|
||||
sendViewModel.memo = ""
|
||||
sendViewModel.includeFromAddress = false
|
||||
onNext()
|
||||
}
|
||||
|
||||
private fun onAddMemo() {
|
||||
sendViewModel.memo = binding.inputMemo.text.toString()
|
||||
onNext()
|
||||
}
|
||||
|
||||
private fun onNext() {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,36 @@
|
|||
package cash.z.ecc.android.ui.send
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.feedback.Feedback
|
||||
import cash.z.ecc.android.feedback.Feedback.Keyed
|
||||
import cash.z.ecc.android.feedback.Feedback.TimeMetric
|
||||
import cash.z.ecc.android.feedback.Report
|
||||
import cash.z.ecc.android.feedback.Report.MetricType
|
||||
import cash.z.ecc.android.feedback.Report.MetricType.*
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||
import cash.z.wallet.sdk.annotation.OpenForTesting
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val metrics = mutableMapOf<String, TimeMetric>()
|
||||
|
||||
@Inject
|
||||
lateinit var lockBox: LockBox
|
||||
|
||||
|
@ -26,6 +40,23 @@ class SendViewModel @Inject constructor() : ViewModel() {
|
|||
@Inject
|
||||
lateinit var initializer: Initializer
|
||||
|
||||
@Inject
|
||||
lateinit var feedback: Feedback
|
||||
|
||||
var fromAddress: String = ""
|
||||
var toAddress: String = ""
|
||||
var memo: String = ""
|
||||
var zatoshiAmount: Long = -1L
|
||||
var includeFromAddress: Boolean = false
|
||||
set(value) {
|
||||
require(!value || (value && !fromAddress.isNullOrEmpty())) {
|
||||
"Error: from address was empty while attempting to include it in the memo. Verify" +
|
||||
" that initFromAddress() has previously been called on this viewmodel."
|
||||
}
|
||||
field = value
|
||||
}
|
||||
val isShielded get() = toAddress.startsWith("z")
|
||||
|
||||
fun send(): Flow<PendingTransaction> {
|
||||
val memoToSend = if (includeFromAddress) "$memo\nsent from\n$fromAddress" else memo
|
||||
val keys = initializer.deriveSpendingKeys(
|
||||
|
@ -41,17 +72,20 @@ class SendViewModel @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun validate() = flow<String?> {
|
||||
suspend fun validateAddress(address: String): Synchronizer.AddressType =
|
||||
synchronizer.validateAddress(address)
|
||||
|
||||
fun validate(maxZatoshi: Long?) = flow<String?> {
|
||||
|
||||
when {
|
||||
synchronizer.validateAddress(toAddress).isNotValid -> {
|
||||
emit("Please enter a valid address")
|
||||
}
|
||||
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> {
|
||||
emit("Please enter a larger amount")
|
||||
emit("Too little! Please enter at least 0.0001")
|
||||
}
|
||||
synchronizer.getAddress() == toAddress -> {
|
||||
emit("That appears to be your address!")
|
||||
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
|
||||
emit( "Too much! Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}")
|
||||
}
|
||||
else -> emit(null)
|
||||
}
|
||||
|
@ -64,16 +98,81 @@ class SendViewModel @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
var fromAddress: String = ""
|
||||
var toAddress: String = ""
|
||||
var memo: String = ""
|
||||
var zatoshiAmount: Long = -1L
|
||||
var includeFromAddress: Boolean = false
|
||||
set(value) {
|
||||
require(!value || (value && !fromAddress.isNullOrEmpty())) {
|
||||
"Error: from address was empty while attempting to include it in the memo. Verify" +
|
||||
" that initFromAddress() has previously been called on this viewmodel."
|
||||
fun reset() {
|
||||
fromAddress = ""
|
||||
toAddress = ""
|
||||
memo = ""
|
||||
zatoshiAmount = -1L
|
||||
includeFromAddress = false
|
||||
}
|
||||
|
||||
fun updateMetrics(tx: PendingTransaction) {
|
||||
try {
|
||||
when {
|
||||
tx.isMined() -> TRANSACTION_SUBMITTED to TRANSACTION_MINED by tx.id
|
||||
tx.isSubmitSuccess() -> TRANSACTION_CREATED to TRANSACTION_SUBMITTED by tx.id
|
||||
tx.isCreated() -> TRANSACTION_INITIALIZED to TRANSACTION_CREATED by tx.id
|
||||
tx.isCreating() -> +TRANSACTION_INITIALIZED by tx.id
|
||||
else -> null
|
||||
}?.let { metricId ->
|
||||
report(metricId)
|
||||
}
|
||||
field = value
|
||||
} catch (t: Throwable) {
|
||||
Crashlytics.logException(RuntimeException("Error while updating Metrics", t))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun report(metricId: String?) {
|
||||
metrics[metricId]?.let { metric ->
|
||||
metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let {
|
||||
viewModelScope.launch {
|
||||
withContext(IO) {
|
||||
feedback.report(metric)
|
||||
|
||||
// does this metric complete another metric?
|
||||
metricId!!.toRelatedMetricId().let { relatedId ->
|
||||
metrics[relatedId]?.let { relatedMetric ->
|
||||
// then remove the related metric, itself. And the relation.
|
||||
metrics.remove(relatedMetric.toMetricIdFor(metricId!!.toTxId()))
|
||||
metrics.remove(relatedId)
|
||||
}
|
||||
}
|
||||
|
||||
// remove all top-level metrics
|
||||
if (metric.key == Report.MetricType.TRANSACTION_MINED.key) metrics.remove(metricId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun MetricType.unaryPlus(): TimeMetric = TimeMetric(key, description).markTime()
|
||||
private infix fun TimeMetric.by(txId: Long) = this.toMetricIdFor(txId).also { metrics[it] = this }
|
||||
private infix fun Pair<MetricType, MetricType>.by(txId: Long): String? {
|
||||
val startMetric = first.toMetricIdFor(txId).let { metricId ->
|
||||
metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") }
|
||||
}
|
||||
return startMetric?.endTime?.let { startMetricEndTime ->
|
||||
TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime))
|
||||
.markTime().let { endMetric ->
|
||||
endMetric.toMetricIdFor(txId).also { metricId ->
|
||||
metrics[metricId] = endMetric
|
||||
metrics[metricId.toRelatedMetricId()] = startMetric
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key"
|
||||
private fun String.toRelatedMetricId(): String = "$this.related"
|
||||
private fun String.toTxId(): Long = split('.').first().toLong()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package cash.z.ecc.android.ui.util
|
||||
//
|
||||
//import android.Manifest
|
||||
//import android.content.Context
|
||||
//import android.content.pm.PackageManager
|
||||
//import android.os.Bundle
|
||||
//import android.widget.Toast
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.fragment.app.Fragment
|
||||
//import cash.z.ecc.android.ui.MainActivity
|
||||
//
|
||||
//class PermissionFragment : Fragment() {
|
||||
//
|
||||
// val activity get() = context as MainActivity
|
||||
//
|
||||
// override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// super.onCreate(savedInstanceState)
|
||||
// if (!hasPermissions(activity)) {
|
||||
// requestPermissions(PERMISSIONS, REQUEST_CODE)
|
||||
// } else {
|
||||
// activity.openCamera()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onRequestPermissionsResult(
|
||||
// requestCode: Int, permissions: Array<String>, grantResults: IntArray
|
||||
// ) {
|
||||
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
//
|
||||
// if (requestCode == REQUEST_CODE) {
|
||||
// if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
// activity.openCamera()
|
||||
// } else {
|
||||
// Toast.makeText(context, "Camera request denied", Toast.LENGTH_LONG).show()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// companion object {
|
||||
// private const val REQUEST_CODE = 101
|
||||
// private val PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
|
||||
//
|
||||
// fun hasPermissions(context: Context) = PERMISSIONS.all {
|
||||
// ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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_dark"/>
|
||||
<item android:state_pressed="true" android:color="@color/text_light" />
|
||||
</selector>
|
|
@ -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_dimmed"/>
|
||||
<item android:state_pressed="true" android:color="@color/text_dark" />
|
||||
</selector>
|
|
@ -1,31 +0,0 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:width="108dp">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:height="16dp">
|
||||
<shape>
|
||||
<corners
|
||||
android:bottomLeftRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="0dp" />
|
||||
<solid android:color="@color/text_light_dimmed" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:end="1dp"
|
||||
android:start="1dp"
|
||||
android:left="1dp"
|
||||
android:right="1dp"
|
||||
android:bottom="1dp"
|
||||
android:height="15dp">
|
||||
<shape>
|
||||
<corners
|
||||
android:bottomLeftRadius="10dp"
|
||||
android:bottomRightRadius="10dp"
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="0dp" />
|
||||
<solid android:color="@color/background_banner" />
|
||||
</shape>
|
||||
</item></layer-list>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="@color/background_banner"
|
||||
android:startColor="@android:color/transparent" />
|
||||
</shape>
|
|
@ -22,9 +22,6 @@
|
|||
android:bottomRightRadius="0dp"
|
||||
android:topLeftRadius="10dp"
|
||||
android:topRightRadius="10dp" />
|
||||
<stroke
|
||||
android:color="#282828"
|
||||
android:width="1dp" />
|
||||
<solid android:color="@color/background_banner" />
|
||||
</shape>
|
||||
</item></layer-list>
|
||||
|
|
|
@ -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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
</vector>
|
|
@ -1,171 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:width="108dp">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
|
@ -0,0 +1,126 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="178dp"
|
||||
android:height="178dp"
|
||||
android:viewportWidth="178"
|
||||
android:viewportHeight="178">
|
||||
<path
|
||||
android:pathData="M89,89m-89,0a89,89 0,1 1,178 0a89,89 0,1 1,-178 0"
|
||||
android:strokeAlpha="0.8"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="0.8"/>
|
||||
<path
|
||||
android:pathData="M31,27C31,23.953 31,19.383 31,13.289C31,7.195 32.391,2.766 35.174,0L55,12.273L31,27Z"
|
||||
android:strokeAlpha="0.8"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="0.8"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M27.234,93.372l71.508,0l0,67.628l-71.508,0z"/>
|
||||
<path
|
||||
android:pathData="M75.534,113.453C76.201,112.487 78.297,111.996 81.78,111.965L82.794,113.875L81.179,112.367L82.303,114.486L80.637,112.932L81.827,115.175L79.798,113.282L81.01,115.568L78.912,113.61L80.103,115.854L77.796,113.703L79.208,116.364L77.435,114.71L78.707,117.106L76.462,115.012L77.883,117.69L76.675,116.563L77.769,118.624L77.719,118.713L76.447,117.525L77.41,119.34C77.399,119.363 77.392,119.387 77.383,119.41L76.224,118.329L77.138,120.052C77.125,120.091 77.117,120.131 77.104,120.17L76.034,119.172L76.922,120.848L76.916,120.881L76.088,120.109L76.798,121.447L76.142,120.834L76.718,121.921C76.699,122.015 76.684,122.116 76.663,122.206L75.873,121.47L76.53,122.706C76.523,122.728 76.518,122.748 76.511,122.77L75.885,122.185L76.395,123.147C76.312,123.365 76.221,123.56 76.124,123.736C75.539,121.693 75.34,118.271 75.534,113.453M95.919,112.818L95.907,112.79L95.894,112.756L95.878,112.717L95.859,112.671L95.839,112.619L95.623,112.09L95.582,111.99L95.539,111.884L95.494,111.772L95.47,111.714L95.446,111.654C93.675,107.322 91.881,103.074 90.257,98.87L90.256,98.866C90.329,100.005 90.347,100.993 90.318,101.845L89.238,99.836L89.266,102.918L88.006,100.847L88.206,103.123L87.041,101.206L87.209,103.133L85.736,100.712L85.905,102.643L83.729,99.063L83.825,100.156L81.647,96.571L81.826,98.625C81.787,98.581 81.752,98.545 81.714,98.503L80.044,95.756L80.167,97.164C80.155,97.159 80.146,97.159 80.134,97.155L78.94,95.61L79.641,98.208C79.701,98.687 79.848,99.327 80.074,100.118L77.399,97.623L80.492,103.449L78.338,101.44L82.342,108.986L80.463,107.235L82.831,111.697L80.469,109.492L81.685,111.784C78.126,111.028 75.317,110.905 73.277,111.431C72.94,116.275 72.994,120.05 73.428,122.79L72.191,120.9L72.925,123.827L71.423,121.369L71.907,123.75L70.496,120.194L70.57,123.537C68.488,115.831 67.015,109.705 67.584,107.235C71.599,106.105 77.505,108.032 80.474,107.59C75.65,105.3 69.32,102.934 63.19,102.374C62.307,109.447 62.98,115.242 65.181,119.787L62.974,117.036L63.541,118.732L61.364,116.024L62.242,118.648C62.156,118.627 62.074,118.614 61.993,118.601L59.571,116.217L60.972,118.686C60.788,118.761 60.626,118.886 60.479,119.044C60.383,118.975 60.289,118.906 60.193,118.841C56.697,111.956 56.997,105.954 58.877,98.167C65.581,98.352 71.753,99.603 77.708,101.662C72.037,97.651 64.551,93.369 54.802,93.372C50.881,101.345 52.661,113.16 56.051,116.948C56.323,117.09 56.57,117.217 56.817,117.344C53.969,123.839 48.623,128.541 43.212,131.859C36.362,130.618 32.826,128.06 27.234,124.417C29.821,128.796 32.849,133.173 36.409,137.55C39.603,138.951 42.808,141.86 47.316,142.272C55.185,137.428 62.058,129.232 63.791,123.912C64.842,126.513 65.777,129.775 66.561,133.913C66.561,133.913 67.404,134.273 68.946,134.578C63.836,141.226 57.075,147.075 51.713,150.472C56.474,153.438 61.638,155.858 67.124,157.632C72.546,151.167 76.642,141.763 78.573,134.044C79.318,134.313 80.164,134.686 81.065,135.099C81.954,135.51 82.728,135.854 83.431,136.132C83.068,143.377 80.126,153.533 76.93,160.047C78.691,160.347 80.475,160.584 82.284,160.754C83.07,160.829 83.852,160.885 84.634,160.933C86.852,153.688 88.265,144.188 87.799,136.689C88.451,136.508 89.125,136.182 89.876,135.69C89.81,145.466 90.666,155.067 92.048,161C93.774,160.926 95.487,160.79 97.181,160.59C94.866,155.316 92.858,145.449 91.551,134.441C91.689,134.328 91.823,134.221 91.968,134.099C93.072,133.169 92.695,132.554 93.525,131.505C94.744,129.968 97.718,129.641 98.346,127.849C99.579,124.337 97.633,118.995 96.602,114.51C96.374,113.944 96.148,113.38 95.919,112.818"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M73.653,44.407C73.653,44.406 73.654,44.406 73.654,44.405L73.554,44.215C73.595,44.254 73.637,44.292 73.679,44.329C73.681,44.363 73.683,44.399 73.684,44.432C73.674,44.424 73.663,44.416 73.653,44.407M87.7,69.284C87.701,69.164 87.703,69.045 87.704,68.926C87.707,69.041 87.703,69.163 87.7,69.284M61.99,32.998C61.703,35.915 58.112,38.538 55.697,42.426C54.823,43.834 54.989,44.767 54.637,46.58C54.181,48.92 53.45,52.304 54.671,55.6C54.907,56.234 56.02,51.16 57.925,46.601C59.004,44.014 62.283,40.502 65.272,38.646C62.085,42.781 59.59,50.671 59.704,56.763C61.543,58.06 64.29,59.572 64.918,61.598C64.395,54.53 66.266,45.281 68.75,39.339C67.865,47.251 67.532,57.47 67.693,63.089C72.924,70.569 79.247,81.626 82.767,93.29C79.337,77.947 75.918,66.805 72.509,62.357C72.488,54.003 72.84,48.925 73.561,44.907C73.616,44.98 73.671,45.055 73.723,45.127C74.229,53.988 74.781,59.365 75.382,61.243C79.702,73.58 82.901,86.212 87.693,102C81.994,78.934 79.386,66.139 77.153,60.754C76.667,56.548 76.426,54.446 76.426,54.446C78.265,58.449 79.187,60.452 79.187,60.452C81.681,69.88 85.208,82.978 89.769,99.743C85.37,79.315 82.569,66.189 81.366,60.363C80.07,56.455 78.706,52.95 77.28,49.819C79.66,52.909 81.648,55.47 83.149,57.928C83.833,59.049 84.232,60.963 84.747,61.899C84.87,62.125 84.973,62.329 85.086,62.544C83.291,69.249 85.173,79.303 90.127,92.673C88.282,84.204 87.785,77.884 87.704,72.544C89.481,70.52 90.532,68.816 90.851,67.433C91.171,66.252 91.065,65.342 90.541,64.7C90.95,64.424 91.319,64.273 91.639,64.272C90.749,63.477 89.918,63.184 89.144,63.352C88.473,62.365 88.085,60.967 87.194,59.914C86.273,58.822 85.75,56.663 84.84,55.366C82.955,52.685 80.599,50.308 78.106,48.108C78.169,48.162 78.232,48.213 78.295,48.267C80.518,49.479 83.274,51.634 85.846,54.462C87.207,55.961 89.137,59.148 90.102,59.939C89.994,59.828 87.828,53.839 87.009,52.76C85.5,50.776 82.024,47.346 78.509,44.128C79.498,44.47 80.549,44.802 80.969,45.178C82.481,46.532 83.389,47.894 83.824,48.504C84.142,48.949 83.734,46.926 82.586,44.439C82.231,43.671 80.125,42.302 79.727,41.694C79.719,41.684 79.708,41.675 79.701,41.666C81.677,41.906 83.696,41.759 85.761,41.172C98.386,39.005 112.126,22.523 106.836,10.884C102.453,12.51 98.424,12.541 94.7,15.262C83.983,23.098 84.821,31.106 80.151,35.415C79.663,35.424 78.616,36.088 78.051,36.145C78.754,34.6 79.421,32.575 80.05,30.06C79.449,31.513 78.658,32.755 77.694,33.8C77.342,34.159 77.035,34.418 76.95,34.27C78.285,32.618 79.513,29.578 80.633,25.107C80.191,26.169 79.662,27.132 79.043,27.995C78.963,28.024 78.881,28.052 78.787,28.082C79.341,26.634 79.87,24.897 80.378,22.869C79.669,24.582 78.724,26.028 77.552,27.218C77.579,27.137 77.608,27.051 77.637,26.964C78.735,24.644 79.771,21.543 80.748,17.643C80.365,18.567 79.93,19.431 79.448,20.244C79.397,20.29 79.338,20.354 79.265,20.455C78.384,21.652 77.687,22.575 77.131,23.288C78.002,21.166 78.832,18.524 79.624,15.362C78.901,17.106 77.993,18.64 76.917,19.985C77.476,18.345 78.018,16.486 78.543,14.389C77.704,16.417 76.618,18.168 75.299,19.657C76.051,17.687 76.773,15.324 77.465,12.563C76.67,14.479 75.662,16.155 74.441,17.588C74.413,17.338 74.389,17.095 74.372,16.864C74.707,14.847 74.913,12.472 74.987,9.733C74.715,11.042 74.295,12.252 73.758,13.385C73.494,11.928 73.048,10.418 72.327,8.889C72.646,10.075 72.644,11.501 72.439,13.014C72.211,12.332 71.95,11.62 71.657,10.877C71.58,10.506 71.47,10.189 71.301,9.983C70.906,9.031 70.479,8.047 69.985,7C70.282,8.1 70.451,9.18 70.517,10.247C70.236,9.602 69.95,8.951 69.625,8.264C69.918,9.349 70.088,10.417 70.156,11.472C69.872,10.816 69.579,10.154 69.25,9.457C69.582,10.691 69.752,11.901 69.795,13.093C69.111,11.809 68.768,11.167 68.768,11.167C68.999,12.776 69.175,14.001 69.302,14.882C69.287,14.937 69.272,14.993 69.256,15.048C68.71,14.025 68.428,13.493 68.428,13.493C68.601,14.701 68.742,15.687 68.856,16.485C68.856,16.486 68.855,16.488 68.855,16.49C68.328,15.502 68.056,14.99 68.056,14.99C68.211,16.077 68.341,16.977 68.448,17.731L67.791,16.496C67.932,17.478 68.051,18.311 68.153,19.021C67.869,20.047 67.678,20.74 67.677,20.753C67.013,27.006 62.565,27.155 61.99,32.998"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M58.315,64.931C58.57,64.567 59.288,64.603 59.918,65.008C60.548,65.414 60.852,66.037 60.597,66.4C60.34,66.764 59.621,66.729 58.992,66.324C58.362,65.917 58.058,65.294 58.315,64.931M56.186,68.69C57.396,71.109 59.935,72.683 65.697,72.002C65.697,72.002 66.806,72.337 69,73C68.429,72.713 67.707,71.701 66.833,69.944C65.232,64.332 60.954,61.017 54,60C54.062,60.394 54.12,60.769 54.178,61.121C54.202,61.282 55.071,61.516 55.094,61.668C55.474,64.084 54.708,65.74 56.186,68.69"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M34.187,48.09C34.699,47.505 35.252,46.937 35.827,46.379C39.045,43.91 47.449,38.323 46.099,44.41L46.1,44.41C44.772,50.782 45.573,58.935 49.047,65C48.162,61.604 47.088,51.546 48.047,48.109C48.103,51.048 48.902,53.723 50.444,56.132C49.121,47.94 51.158,40.913 54.072,37.761L54.069,37.761C55.832,36.521 57.143,35.075 58,33.424C55.922,35.53 53.808,33.017 53.75,30.109C53.85,24.838 54.635,19.548 53.909,16.361C52.479,10.088 45.465,7.971 36.4,2C29.204,16.179 30.835,28.659 41.292,39.441C38.393,41.563 36.024,44.446 34.187,48.09"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M53.798,85.526C59.921,89.794 68.125,86.664 78,92.83C68.278,82.595 55.752,83.954 51.506,79C42.743,89.786 43.216,100.904 50.626,111.498C53.373,112.5 54.746,113 54.746,113C48.652,103.762 48.11,94.172 53.798,85.526"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M31.578,50.359C33.086,56.237 32.536,63.28 29.93,71.49C25.876,85.045 26.03,94.775 30.388,100.679C32.983,103.559 34.28,105 34.28,105C29.221,95.598 28.403,84.998 31.825,73.2C34.744,64.577 35.59,57.011 34.364,50.505C36.893,47.549 39.771,45.047 43,43C38.756,44.414 34.949,46.866 31.578,50.359"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M38.879,51.831C39.596,55.815 39.206,61.663 37.71,69.375C34.935,77.256 33.717,84.464 34.055,91C34.774,83.862 36.236,78.48 38.442,74.855C40.141,71.09 41.066,66.309 41.216,60.511C40.563,56.326 40.824,52.156 42,48C40.689,49.515 39.65,50.793 38.879,51.831"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M27.141,73.7C29.222,68.502 30.508,61.935 31,54C24.202,64.678 22.354,77.013 25.461,91C24.453,85.402 25.014,79.636 27.141,73.7"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M45.733,110.523L43.589,109.584C44.373,109.887 45.206,110.214 46.094,110.681C31.329,94.039 44.903,85.505 47.749,72.734C54.47,80.172 68.209,77.553 78.659,92.81C76.154,84.636 74.452,78.41 71.457,74.616C70.996,79.433 72.058,79.056 67.134,77.488C60.86,75.489 56.635,72.961 53.293,70.632C45.734,65.365 45.24,52.526 45.171,44.061C43.847,51.726 43.464,57.497 43.815,61.917L43.812,61.918C44.73,79.047 26.921,92.175 39.528,107.185C40.033,107.787 40.611,108.206 41.239,108.554L41.134,108.509C27.843,101.921 19.614,91.74 21.686,74.951C20.136,83.355 19.655,90.108 20.242,95.211C26.209,105.144 35.668,110.692 45.733,110.523"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M53,113.727L49.638,112.538C44.547,113.9 42.906,115.752 40.751,115.873C33.032,112.836 25.126,108.138 21,104C22.007,108.368 22.96,111.661 23.863,113.879C26.193,116.985 30.638,119.635 40.599,123C44.757,120.785 49.07,118.216 53,113.727"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M107.716,134.26C107.657,114.194 101.418,100.109 89,92C100.713,108.476 104.469,123.052 100.268,135.729C99.051,146.888 99.456,154.978 101.481,160C107.334,158.816 111.507,157.722 114,156.716C109.043,150.659 106.949,143.172 107.716,134.26"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M98.922,97.522L97,97C112.927,117.911 110.819,134.785 115.876,156C119.206,154.43 121.58,153.193 123,152.29C115.335,139.485 114.839,111.646 98.922,97.522"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M124,100.118C124,100.118 121.555,100.071 119.633,100.017L119.226,100C116.601,100.242 114.132,101.081 113,101.831C113.734,102.608 121.644,100.477 124,100.118"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M150,99.383C150,99.383 147.997,99.618 143.992,100.089C136.388,102.524 131.08,112.385 129.335,125.752C127.678,122.428 125.754,119.184 123.629,116.018C128.03,108.975 133.641,102.701 141.679,100.312C136.915,100.736 132.474,100.951 128.351,100.952C125.471,103.02 122.861,106.578 120.523,111.625L120.532,111.646C117.258,107.287 113.592,103.096 109.628,99.103C109.628,99.103 109.519,99.078 105,98C117.168,110.365 125.273,125.387 129.314,143.062C129.885,145.554 132.639,146.402 133.048,149C135.687,147.088 141.795,141.023 145.163,136.929C140.259,132.813 139.62,127.74 138.231,123.622C138.781,113.038 141.659,105.258 150,99.383"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M146.012,98.002l12.862,0l0,33.828l-12.862,0z"/>
|
||||
<path
|
||||
android:pathData="M155.632,98.563C147.497,106.769 144.577,118.826 146.66,131.829C153.108,124.925 157.111,112.683 158.874,98.002C158.874,98.002 157.793,98.189 155.632,98.563"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M60.912,25.072C60.709,28.607 60.579,31.698 59.85,33C62.873,28.076 63.277,21.836 62.865,16L62.115,18.947L61.813,16.538L61.576,19.573L60.873,17.265L60.85,17.286L60.571,20.852L59.824,18.4L59.668,20.386L59.225,18.932L59,21.815C59.562,23.849 59.865,26.083 59.911,28.52C60.579,26.22 60.912,25.072 60.912,25.072"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
|
@ -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="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/background_footer" />
|
|
@ -101,6 +101,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="(enter an amount to send)"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="@color/text_light_dimmed"
|
||||
app:layout_constraintBottom_toBottomOf="@id/back_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -110,10 +111,10 @@
|
|||
android:id="@+id/header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_header"
|
||||
android:onClick="copyAddress"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingTop="24dp"
|
||||
android:onClick="copyAddress"
|
||||
android:background="@drawable/background_header"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/back_button_hit_area">
|
||||
|
@ -144,28 +145,51 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="zs1g7cqw...9qmvyzgm"
|
||||
android:textColor="@color/colorPrimaryMedium"
|
||||
app:layout_constraintStart_toEndOf="@id/label_address"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_header_title" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_header_title"
|
||||
tools:text="zs1g7cqw...9qmvyzgm" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_copy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:src="@drawable/ic_content_copy"
|
||||
app:layout_constraintTop_toTopOf="@id/label_address"
|
||||
app:layout_constraintBottom_toBottomOf="@id/label_address"
|
||||
app:layout_constraintStart_toEndOf="@id/text_address"
|
||||
app:layout_constraintDimensionRatio="W,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.15"
|
||||
app:layout_constraintDimensionRatio="W,1:1"/>
|
||||
app:layout_constraintStart_toEndOf="@id/text_address"
|
||||
app:layout_constraintTop_toTopOf="@id/label_address" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/empty_footer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="16dp"
|
||||
android:background="@drawable/background_footer"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/header" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="No history yet."
|
||||
android:textColor="@color/text_light"
|
||||
android:textSize="18dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_transactions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
|
||||
|
@ -173,6 +197,24 @@
|
|||
tools:itemCount="15"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_transaction"
|
||||
tools:orientation="vertical" />
|
||||
tools:orientation="vertical"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/footer_fade"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:alpha="0.8"
|
||||
android:background="@drawable/background_gradient_bottom"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_empty_views"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="empty_footer,empty_message" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -241,23 +241,17 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/button_number_pad_9"
|
||||
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
|
||||
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_send"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
style="@style/Zcash.Button"
|
||||
android:text="Send Amount"
|
||||
android:enabled="false"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="#000000"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/guide_keys"
|
||||
app:layout_constraintStart_toStartOf="@id/guide_keys"
|
||||
app:layout_constraintTop_toBottomOf="@id/guide_keys"/>
|
||||
<!-- <com.google.android.material.button.MaterialButton-->
|
||||
<!-- android:id="@+id/button_send"-->
|
||||
<!-- android:layout_width="0dp"-->
|
||||
<!-- android:layout_height="0dp"-->
|
||||
<!-- style="@style/Zcash.Button"-->
|
||||
<!-- android:text=""-->
|
||||
<!-- android:enabled="false"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="@id/guide_keys"-->
|
||||
<!-- app:layout_constraintStart_toStartOf="@id/guide_keys"-->
|
||||
<!-- app:layout_constraintTop_toTopOf="@id/lottie_button_loading"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="@id/lottie_button_loading"/>-->
|
||||
|
||||
<View
|
||||
android:id="@+id/layer_lock"
|
||||
|
@ -271,6 +265,41 @@
|
|||
<!-- -->
|
||||
<!-- Upper Layer -->
|
||||
<!-- -->
|
||||
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie_button_loading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:elevation="6dp"
|
||||
app:layout_constraintHeight_percent="0.075"
|
||||
app:layout_constraintWidth_percent="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
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:lottie_autoPlay="false"
|
||||
app:lottie_loop="false"
|
||||
app:lottie_rawRes="@raw/lottie_button_loading_new" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_send_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Downloading...5%"
|
||||
android:elevation="6dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingStart="56dp"
|
||||
android:paddingEnd="56dp"
|
||||
android:textColor="@color/selector_button_text_dark"
|
||||
android:textSize="16dp"
|
||||
app:layout_constraintTop_toTopOf="@id/lottie_button_loading"
|
||||
app:layout_constraintBottom_toBottomOf="@id/lottie_button_loading"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_scan"
|
||||
android:layout_width="0dp"
|
||||
|
@ -280,7 +309,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.052"
|
||||
app:layout_constraintHorizontal_bias="0.108"
|
||||
app:layout_constraintHorizontal_bias="0.088"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0574"
|
||||
|
@ -296,7 +325,7 @@
|
|||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.8883"
|
||||
app:layout_constraintHorizontal_bias="0.912"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.064"
|
||||
app:layout_constraintWidth_percent="0.08"
|
||||
|
@ -351,7 +380,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon_detail"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_send"
|
||||
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading"
|
||||
app:layout_constraintVertical_bias="@dimen/ratio_golden_small" />
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.8883"
|
||||
app:layout_constraintHorizontal_bias="0.912"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.064"
|
||||
|
@ -56,7 +56,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:elevation="6dp"
|
||||
android:tint="@color/text_light_dimmed"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -64,7 +63,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.212"
|
||||
app:layout_constraintWidth_percent="0.4"
|
||||
app:srcCompat="@drawable/ic_account_circle" />
|
||||
app:srcCompat="@drawable/ic_profile_zebra_01" />
|
||||
|
||||
<View
|
||||
android:id="@+id/hit_area_close"
|
||||
|
@ -80,6 +79,7 @@
|
|||
android:id="@+id/text_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textSize="20dp"
|
||||
android:text="Shielded User"
|
||||
|
@ -162,6 +162,7 @@
|
|||
style="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textSize="16sp"
|
||||
android:text="See Application Log"
|
||||
android:textColor="@color/selector_button_text_light_dimmed"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_backup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
@ -181,7 +182,7 @@
|
|||
android:paddingTop="8dp"
|
||||
android:text="zECC App"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
android:textColor="@color/selector_button_text_light"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_logs"
|
||||
|
|
|
@ -183,7 +183,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
|
||||
app:srcCompat="@drawable/ic_close_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
|
||||
app:srcCompat="@drawable/ic_close_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_home"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
android:background="@drawable/background_home">
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
|
@ -13,13 +13,13 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="@color/text_light"
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.05"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
app:layout_constraintHorizontal_bias="0.05"/>
|
||||
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/back_button_hit_area"
|
||||
|
@ -38,71 +38,97 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
tools:text="Sending 12.34121212 ZEC"
|
||||
android:textColor="@color/text_light"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
|
||||
android:text="Sending"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="@id/back_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/back_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/back_button" />
|
||||
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
|
||||
app:layout_constraintTop_toTopOf="@id/back_button" />
|
||||
|
||||
<!-- Input: Address -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:hint="@string/send_hint_input_zcash_address"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:paddingRight="76dp"
|
||||
android:singleLine="true"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="@color/text_light"
|
||||
app:backgroundTint="@color/colorPrimary"
|
||||
android:textColorHint="@color/text_light_dimmed"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.84"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
|
||||
<!-- Input: Amount -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_amount"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_layout_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:hint="@string/send_hint_input_zcash_amount"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="numberDecimal"
|
||||
android:paddingRight="76dp"
|
||||
android:singleLine="true"
|
||||
android:paddingTop="0dp"
|
||||
android:textColor="@color/text_light"
|
||||
app:backgroundTint="@color/colorPrimary"
|
||||
android:textColorHint="@color/text_light_dimmed"
|
||||
app:layout_constraintTop_toBottomOf="@id/input_zcash_address"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:hint="To"
|
||||
android:theme="@style/Zcash.Overlay.TextInputLayout"
|
||||
app:endIconDrawable="@drawable/ic_qrcode_24dp"
|
||||
app:endIconMode="custom"
|
||||
app:helperText="Enter a valid Zcash address"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.84"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_banner_message"
|
||||
app:layout_constraintVertical_bias="0.08"
|
||||
app:layout_constraintWidth_percent="0.84">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_zcash_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true"
|
||||
android:maxLength="255"
|
||||
android:textColor="@color/text_light"
|
||||
android:textColorHint="@color/text_light_dimmed" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Input: Amount -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_layout_amount"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:hint="Amount"
|
||||
android:theme="@style/Zcash.Overlay.TextInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_layout_address"
|
||||
app:layout_constraintWidth_percent="0.84"
|
||||
tools:helperText="You have 23.23 ZEC available">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/input_zcash_amount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="numberDecimal"
|
||||
android:maxLength="20"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/text_light"
|
||||
android:textColorHint="@color/text_light_dimmed" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_max"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:padding="16dp"
|
||||
android:text="MAX"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_layout_amount"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_layout_amount"
|
||||
app:layout_constraintTop_toTopOf="@id/text_layout_amount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_error"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:textColor="@android:color/holo_red_light"
|
||||
android:maxLines="1"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/zcashRed"
|
||||
android:textSize="14dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/button_next"
|
||||
app:layout_constraintStart_toStartOf="@+id/input_zcash_amount"
|
||||
app:layout_constraintTop_toTopOf="@+id/button_next"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_next"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/text_layout_amount"
|
||||
app:layout_constraintTop_toBottomOf="@+id/button_next"
|
||||
app:layout_constraintVertical_bias="0.1"
|
||||
tools:text="Please enter a larger amount of money also please enter a shorter sentence" />
|
||||
|
||||
<!-- Scan QR code -->
|
||||
|
@ -110,14 +136,15 @@
|
|||
android:id="@+id/image_scan_qr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="24dp"
|
||||
android:paddingEnd="1dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="1dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:tint="@color/zcashWhite"
|
||||
app:layout_constraintBottom_toBottomOf="@id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/input_zcash_address"
|
||||
app:layout_constraintTop_toTopOf="@id/input_zcash_address"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_layout_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_layout_address"
|
||||
app:layout_constraintTop_toTopOf="@id/text_layout_address"
|
||||
app:srcCompat="@drawable/ic_qrcode_24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
@ -127,8 +154,8 @@
|
|||
android:layout_marginTop="16dp"
|
||||
android:text="Next"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
|
||||
app:layout_constraintTop_toBottomOf="@+id/input_zcash_amount" />
|
||||
app:layout_constraintEnd_toEndOf="@+id/text_layout_address"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_layout_amount" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Banner -->
|
||||
|
@ -146,19 +173,17 @@
|
|||
android:text="Address on clipboard!"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@+id/text_layout_address"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/input_zcash_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_next"
|
||||
app:layout_constraintVertical_bias="0.07" />
|
||||
app:layout_constraintStart_toStartOf="@+id/text_layout_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/back_button_hit_area" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_banner_action"
|
||||
android:elevation="6dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:elevation="6dp"
|
||||
android:text="Paste"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/colorPrimary"
|
||||
|
@ -169,8 +194,8 @@
|
|||
android:id="@+id/group_banner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="text_banner_message, text_banner_action"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="text_banner_message, text_banner_action"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -81,6 +81,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="Tap\nto send ZEC"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.065"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_close_black_24dp" />
|
||||
|
||||
<View
|
||||
|
@ -72,7 +73,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_dark"
|
||||
tools:text="Creating transaction..."
|
||||
android:textSize="16dp"
|
||||
android:gravity="center"
|
||||
android:textSize="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/radio_include_address"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -104,6 +106,21 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_retry"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
style="@style/Zcash.Button.OutlinedButton"
|
||||
app:strokeColor="@color/text_dark"
|
||||
android:padding="12dp"
|
||||
android:text="Retry"
|
||||
android:visibility="gone"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@id/guide_keys"
|
||||
app:layout_constraintStart_toStartOf="@id/guide_keys"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_next"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
|
@ -113,6 +130,7 @@
|
|||
app:strokeColor="@color/text_dark"
|
||||
android:padding="12dp"
|
||||
android:text="Finished"
|
||||
android:visibility="gone"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@id/guide_keys"
|
||||
app:layout_constraintStart_toStartOf="@id/guide_keys"
|
||||
|
|
|
@ -41,13 +41,14 @@
|
|||
android:background="@drawable/background_banner"
|
||||
android:elevation="6dp"
|
||||
android:gravity="top"
|
||||
android:imeActionLabel="add memo"
|
||||
android:inputType="textImeMultiLine"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="Add a memo here"
|
||||
android:maxLines="3"
|
||||
android:imeActionLabel="add memo"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textImeMultiLine"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/text_light"
|
||||
|
@ -65,38 +66,39 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="6dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:textColor="@color/text_light_dimmed"
|
||||
tools:text="sent from z23lk4jjl2k3jl43kkj542l3kl4hj2l3k1j41l2kjk423lkj423lklhk2jrhiuhrh2j4hh2hkj23hkj4"
|
||||
app:layout_constraintStart_toStartOf="@id/input_memo"
|
||||
app:layout_constraintBottom_toBottomOf="@id/input_memo"
|
||||
app:layout_constraintEnd_toEndOf="@id/input_memo"
|
||||
app:layout_constraintBottom_toBottomOf="@id/input_memo" />
|
||||
app:layout_constraintStart_toStartOf="@id/input_memo"
|
||||
tools:text="sent from z23lk4jjl2k3jl43kkj542l3kl4hj2l3k1j41l2kjk423lkj423lklhk2jrhiuhrh2j4hh2hkj23hkj4" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1px"
|
||||
android:elevation="6dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@color/text_light_dimmed"
|
||||
app:layout_constraintStart_toStartOf="@id/text_included_address"
|
||||
android:elevation="6dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_included_address"
|
||||
app:layout_constraintTop_toTopOf="@id/text_included_address"/>
|
||||
app:layout_constraintStart_toStartOf="@id/text_included_address"
|
||||
app:layout_constraintTop_toTopOf="@id/text_included_address" />
|
||||
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_include_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:padding="0dp"
|
||||
android:layout_marginRight="0dp"
|
||||
android:text="Include your sending address in memo"
|
||||
app:layout_constraintStart_toStartOf="@+id/input_memo"
|
||||
app:layout_constraintTop_toBottomOf="@+id/input_memo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_info_shielded"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
|
@ -108,6 +110,67 @@
|
|||
app:layout_constraintStart_toStartOf="@id/input_memo"
|
||||
app:layout_constraintTop_toBottomOf="@id/check_include_address" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sad_icon"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:elevation="6dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
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" />
|
||||
|
||||
<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: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"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sad_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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:textColor="@color/text_light"
|
||||
android:textSize="18dp"
|
||||
android:layout_marginTop="16dp"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/sad_description"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_next"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_next"
|
||||
android:layout_width="0dp"
|
||||
|
@ -134,4 +197,17 @@
|
|||
app:layout_constraintEnd_toEndOf="@id/button_next"
|
||||
app:layout_constraintStart_toStartOf="@id/button_next"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_next" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/group_transparent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -118,6 +118,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingStart="12dp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
|
@ -127,6 +128,6 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/indicator"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
tools:text="+ 4345.2444" />
|
||||
tools:text="+ 434.2444234" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +0,0 @@
|
|||
<?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>
|
|
@ -1,6 +0,0 @@
|
|||
<?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: 3.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -40,7 +40,7 @@
|
|||
<action
|
||||
android:id="@+id/action_nav_receive_to_nav_scan"
|
||||
app:destination="@id/nav_scan"
|
||||
app:popUpTo="@id/nav_scan"
|
||||
app:popUpTo="@id/nav_receive"
|
||||
app:popUpToInclusive="true"
|
||||
app:exitAnim="@anim/anim_fade_out_address"
|
||||
app:enterAnim="@anim/anim_fade_in_scanner"/>
|
||||
|
@ -52,10 +52,11 @@
|
|||
<action
|
||||
android:id="@+id/action_nav_scan_to_nav_send_address"
|
||||
app:destination="@id/nav_send_address"
|
||||
app:popUpTo="@id/nav_send_address"/>
|
||||
app:popUpTo="@id/nav_scan"
|
||||
app:popUpToInclusive="true"/>
|
||||
<action
|
||||
android:id="@+id/action_nav_scan_to_nav_receive"
|
||||
app:popUpTo="@id/nav_receive"
|
||||
app:popUpTo="@id/nav_scan"
|
||||
app:popUpToInclusive="true"
|
||||
app:destination="@id/nav_receive"
|
||||
app:exitAnim="@anim/anim_fade_out_medium"/>
|
||||
|
@ -134,6 +135,11 @@
|
|||
android:id="@+id/nav_send_final"
|
||||
android:name="cash.z.ecc.android.ui.send.SendFinalFragment"
|
||||
tools:layout="@layout/fragment_send_final" >
|
||||
<action
|
||||
android:id="@+id/action_nav_send_final_to_nav_home"
|
||||
app:destination="@id/nav_home"
|
||||
app:popUpTo="@id/nav_home"
|
||||
app:popUpToInclusive="true"/>
|
||||
</fragment>
|
||||
|
||||
|
||||
|
@ -157,4 +163,16 @@
|
|||
android:name="cash.z.ecc.android.ui.setup.BackupFragment"
|
||||
tools:layout="@layout/fragment_backup" >
|
||||
</fragment>
|
||||
|
||||
|
||||
<!-- -->
|
||||
<!-- Global actions -->
|
||||
<!-- -->
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_nav_scan"
|
||||
app:destination="@id/nav_scan"
|
||||
app:popUpTo="@id/nav_scan"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1 @@
|
|||
{"v":"5.6.0","fr":30,"ip":0,"op":30,"w":324,"h":64,"nm":"BalanceHome","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":79,"s":[0]},{"t":90,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":127,"st":30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[4]},{"t":90,"s":[2]}],"ix":5},"lc":2,"lj":1,"ml":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"t":85,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[15]},{"t":85,"s":[100]}],"ix":2},"o":{"a":0,"k":-90,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":30,"op":127,"st":30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162,-201.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.292],[16.292,0],[0,0],[0,16.292],[-16.292,0],[0,0]],"o":[[0,16.292],[0,0],[-16.292,0],[0,-16.292],[0,0],[16.292,0]],"v":[[160,0],[130.5,29.5],[-130.5,29.5],[-160,0],[-130.5,-29.5],[130.5,-29.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.72549021244,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,233.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":30,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"t":30,"s":[15]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-90]},{"t":30,"s":[270]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":30,"st":0,"bm":0}],"markers":[]}
|
|
@ -24,6 +24,7 @@
|
|||
and should map directly to the design style guide -->
|
||||
|
||||
<color name="zcashWhite">#F5F5F5</color>
|
||||
<color name="zcashGray">#616161</color>
|
||||
<color name="zcashWhite_12">#1FFFFFFF</color>
|
||||
<color name="zcashWhite_24">#3DFFFFFF</color>
|
||||
<color name="zcashWhite_40">#66FFFFFF</color>
|
||||
|
@ -44,9 +45,9 @@
|
|||
<color name="zcashBlue">#26DAB6</color>
|
||||
|
||||
<!-- yellows -->
|
||||
<color name="zcashYellow_light">#FFD649</color>
|
||||
<color name="zcashYellow">#FFB727</color>
|
||||
<color name="zcashYellow_dark">#FFA918</color>
|
||||
<color name="zcashYellow_light">#FFCF4A</color>
|
||||
<color name="zcashYellow">#FFB900</color>
|
||||
<color name="zcashYellow_dark">#FFAA17</color>
|
||||
|
||||
|
||||
<!-- -->
|
||||
|
@ -59,6 +60,7 @@
|
|||
<color name="background_banner">@color/zcashBlack_dark</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>
|
||||
|
||||
<!-- text -->
|
||||
<color name="text_light">#FFFFFF</color>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<style name="Zcash.TextAppearance.NumberPad" parent="TextAppearance.MaterialComponents.Body1">
|
||||
<item name="android:textSize">30dp</item>
|
||||
<item name="android:textColor">@color/selector_button_text</item>
|
||||
<item name="android:textColor">@color/selector_button_text_light</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.TextAppearance.AddressPart" parent="TextAppearance.AppCompat">
|
||||
|
@ -75,4 +75,13 @@
|
|||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">24dp</item>
|
||||
</style>
|
||||
<style name="Zcash.ShapeAppearance.TextInputLayout" parent="ShapeAppearance.MaterialComponents.SmallComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">8dp</item>
|
||||
</style>
|
||||
|
||||
<!-- Theme Overlays -->
|
||||
<style name="Zcash.Overlay.TextInputLayout" parent="ThemeOverlay.MaterialComponents">
|
||||
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.TextInputLayout</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package cash.z.ecc.android
|
||||
|
||||
import cash.z.ecc.android.feedback.Feedback
|
||||
import cash.z.ecc.android.ui.send.SendViewModel
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.newSingleThreadContext
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.Spy
|
||||
|
||||
class SendViewModelTest {
|
||||
|
||||
@Mock lateinit var creatingTx: PendingTransaction
|
||||
@Mock lateinit var createdTx: PendingTransaction
|
||||
@Mock lateinit var submittedTx: PendingTransaction
|
||||
@Mock lateinit var minedTx: PendingTransaction
|
||||
|
||||
@Mock
|
||||
lateinit var feedback: Feedback
|
||||
|
||||
@Spy
|
||||
lateinit var sendViewModel: SendViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
Dispatchers.setMain(newSingleThreadContext("Main thread"))
|
||||
|
||||
whenever(creatingTx.id).thenReturn(7)
|
||||
whenever(creatingTx.submitAttempts).thenReturn(0)
|
||||
|
||||
whenever(createdTx.id).thenReturn(7)
|
||||
whenever(createdTx.raw).thenReturn(byteArrayOf(0x1))
|
||||
|
||||
whenever(submittedTx.id).thenReturn(7)
|
||||
whenever(submittedTx.raw).thenReturn(byteArrayOf(0x1))
|
||||
whenever(submittedTx.submitAttempts).thenReturn(1)
|
||||
|
||||
whenever(minedTx.id).thenReturn(7)
|
||||
whenever(minedTx.raw).thenReturn(byteArrayOf(0x1))
|
||||
whenever(minedTx.submitAttempts).thenReturn(1)
|
||||
whenever(minedTx.minedHeight).thenReturn(500_001)
|
||||
|
||||
sendViewModel.feedback = feedback
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateMetrics_creating() {
|
||||
// doNothing().whenever(sendViewModel).report(any())
|
||||
|
||||
assertEquals(true, creatingTx.isCreating())
|
||||
sendViewModel.updateMetrics(creatingTx)
|
||||
|
||||
verify(sendViewModel).report("7.metric.tx.initialized")
|
||||
assertEquals(1, sendViewModel.metrics.size)
|
||||
verifyZeroInteractions(feedback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateMetrics_created() {
|
||||
assertEquals(false, createdTx.isCreating())
|
||||
assertEquals(true, createdTx.isCreated())
|
||||
sendViewModel.updateMetrics(creatingTx)
|
||||
sendViewModel.updateMetrics(createdTx)
|
||||
Thread.sleep(100)
|
||||
println(sendViewModel.metrics)
|
||||
|
||||
verify(sendViewModel).report("7.metric.tx.created")
|
||||
assertEquals(1, sendViewModel.metrics.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateMetrics_submitted() {
|
||||
assertEquals(false, submittedTx.isCreating())
|
||||
assertEquals(false, submittedTx.isCreated())
|
||||
assertEquals(true, submittedTx.isSubmitSuccess())
|
||||
sendViewModel.updateMetrics(creatingTx)
|
||||
sendViewModel.updateMetrics(createdTx)
|
||||
sendViewModel.updateMetrics(submittedTx)
|
||||
assertEquals(5, sendViewModel.metrics.size)
|
||||
|
||||
Thread.sleep(100)
|
||||
assertEquals(1, sendViewModel.metrics.size)
|
||||
|
||||
verify(feedback).report(sendViewModel.metrics.values.first())
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testUpdateMetrics_mined() {
|
||||
assertEquals(true, minedTx.isMined())
|
||||
assertEquals(true, minedTx.isSubmitSuccess())
|
||||
sendViewModel.updateMetrics(creatingTx)
|
||||
sendViewModel.updateMetrics(createdTx)
|
||||
sendViewModel.updateMetrics(submittedTx)
|
||||
sendViewModel.updateMetrics(minedTx)
|
||||
assertEquals(7, sendViewModel.metrics.size)
|
||||
|
||||
Thread.sleep(100)
|
||||
assertEquals(0, sendViewModel.metrics.size)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
mock-maker-inline
|
|
@ -4,11 +4,16 @@ buildscript {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.fabric.io/public'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.0-rc01'
|
||||
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'
|
||||
classpath 'com.google.firebase:perf-plugin:1.3.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package cash.z.ecc.android.feedback
|
||||
|
||||
import android.util.Log
|
||||
import cash.z.ecc.android.feedback.util.CompositeJob
|
||||
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) {
|
||||
|
@ -34,8 +37,17 @@ class Feedback(capacity: Int = 256) {
|
|||
* [actions] channels will remain open unless [stop] is also called on this instance.
|
||||
*/
|
||||
suspend fun start(): Feedback {
|
||||
check(!::scope.isInitialized) {
|
||||
"Error: cannot initialize feedback because it has already been initialized."
|
||||
val callStack = StringBuilder().let { s ->
|
||||
Thread.currentThread().stackTrace.forEach {element ->
|
||||
s.append("$element\n")
|
||||
}
|
||||
s.toString()
|
||||
}
|
||||
if(::scope.isInitialized) {
|
||||
Log.e("@TWIG","Warning: did not initialize feedback because it has already been initialized. Call stack: $callStack")
|
||||
return this
|
||||
} else {
|
||||
Log.e("@TWIG","Debug: Initializing feedback for the first time. Call stack: $callStack")
|
||||
}
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob(coroutineContext[Job]))
|
||||
invokeOnCompletion {
|
||||
|
@ -162,8 +174,8 @@ class Feedback(capacity: Int = 256) {
|
|||
}
|
||||
|
||||
|
||||
interface Metric : Mappable<String, Any> {
|
||||
val key: String
|
||||
interface Metric : Mappable<String, Any>, Keyed<String> {
|
||||
override val key: String
|
||||
val startTime: Long?
|
||||
val endTime: Long?
|
||||
val elapsedTime: Long?
|
||||
|
@ -180,13 +192,17 @@ class Feedback(capacity: Int = 256) {
|
|||
}
|
||||
}
|
||||
|
||||
interface Action : Feedback.Mappable<String, Any> {
|
||||
val key: String
|
||||
interface Action : Feedback.Mappable<String, Any>, Keyed<String> {
|
||||
override val key: String
|
||||
override fun toMap(): Map<String, Any> {
|
||||
return mapOf("key" to key)
|
||||
}
|
||||
}
|
||||
|
||||
interface Keyed<T> {
|
||||
val key: T
|
||||
}
|
||||
|
||||
interface Mappable<K, V> {
|
||||
fun toMap(): Map<K, V>
|
||||
}
|
||||
|
|