Merge pull request #96 from zcash/release/internal-20200325

Internal ECC release 2020-03-25
This commit is contained in:
Kevin Gorham 2020-03-27 16:58:02 -04:00 committed by GitHub
commit 337a361ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 575 additions and 111 deletions

View File

@ -11,7 +11,7 @@ apply plugin: 'com.google.firebase.firebase-perf'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha23'
version = '1.0.0-alpha25'
android {
compileSdkVersion Deps.compileSdkVersion
@ -21,7 +21,7 @@ android {
applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_023
versionCode = 1_00_00_025
// last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX) dev(9XX). 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'

View File

@ -17,7 +17,7 @@ class SynchronizerModule {
@Provides
@SynchronizerScope
fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer {
return Synchronizer(appContext, initializer)
return Synchronizer(initializer)
}
}

View File

@ -5,7 +5,7 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.toSpannable
fun String.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable {
fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable {
return toSpannable().apply {
val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -22,6 +22,10 @@ fun View.disabledIf(isDisabled: Boolean) {
isEnabled = !isDisabled
}
fun View.transparentIf(isTransparent: Boolean) {
alpha = if (isTransparent) 0.0f else 1.0f
}
fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
setOnClickListener {
block()

View File

@ -43,6 +43,12 @@ object Report {
object ImportCompleted : Restore("importcompleted", 50)
object Success : Restore("success", 100)
}
sealed class UserFeedback(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Feedback.Funnel("feedback", stepName, step, *properties) {
object Started : UserFeedback("started", 0)
object Cancelled : UserFeedback("cancelled", 1)
class Submitted(rating: Int, question1: String, question2: String, question3: String) : UserFeedback("submitted", 100, "rating" to rating, "question1" to question1, "question2" to question2, "question3" to question3)
}
}
object Error {
@ -84,6 +90,7 @@ object Report {
DETAIL("wallet.detail"),
LANDING,
PROFILE,
FEEDBACK,
RECEIVE,
RESTORE,
SCAN,
@ -120,6 +127,8 @@ object Report {
PROFILE_VIEW_USER_LOGS("profile.view.user.logs"),
PROFILE_VIEW_DEV_LOGS("profile.view.dev.logs"),
PROFILE_SEND_FEEDBACK("profile.send.feedback"),
FEEDBACK_CANCEL("feedback.cancel"),
FEEDBACK_SUBMIT("feedback.submit"),
RECEIVE_SCAN("receive.scan"),
RECEIVE_BACK("receive.back"),
RESTORE_DONE("restore.done"),

View File

@ -50,7 +50,6 @@ import javax.inject.Inject
class MainActivity : AppCompatActivity() {
@Inject
lateinit var feedback: Feedback
@ -324,6 +323,7 @@ class MainActivity : AppCompatActivity() {
showSnackbar("Well, this is awkward. You denied permission for the camera.")
}
// TODO: clean up this error handling
private var ignoredErrors = 0
private fun onProcessorError(error: Throwable?): Boolean {
var notified = false
@ -368,25 +368,25 @@ class MainActivity : AppCompatActivity() {
}
if (!notified) {
ignoredErrors++
}
if (ignoredErrors >= ZcashSdk.RETRIES) {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Processor Error")
.setMessage(error?.message ?: "Critical error while processing blocks!")
.setCancelable(false)
.setPositiveButton("Retry") { d, _ ->
d.dismiss()
dialog = null
}
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
throw error
?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
}
.show()
if (ignoredErrors >= ZcashSdk.RETRIES) {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Processor Error")
.setMessage(error?.message ?: "Critical error while processing blocks!")
.setCancelable(false)
.setPositiveButton("Retry") { d, _ ->
d.dismiss()
dialog = null
}
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
throw error
?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
}
.show()
}
}
}
}

View File

@ -2,11 +2,15 @@ package cash.z.ecc.android.ui.detail
import android.view.View
import android.widget.TextView
import android.widget.Toast
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.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -21,6 +25,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
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())
private val addressRegex = """zs\d\w{65,}""".toRegex()
fun bindTo(transaction: T?) {
@ -29,13 +34,19 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
var lineTwo: String = ""
var amountZec: String = ""
var amountDisplay: String = ""
var amountColor: Int = 0
var indicatorBackground: Int = 0
var amountColor: Int = R.color.text_light_dimmed
var lineOneColor: Int = R.color.text_light
var lineTwoColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = R.drawable.background_indicator_unknown
transaction?.apply {
itemView.setOnClickListener {
onTransactionClicked(this)
}
itemView.setOnLongClickListener {
onTransactionLongPressed(this)
true
}
amountZec = value.convertZatoshiToZecString()
// TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L)
@ -45,11 +56,16 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
amountDisplay = "- $amountZec"
amountColor = R.color.zcashRed
indicatorBackground = R.drawable.background_indicator_outbound
if (isMined) {
amountColor = R.color.zcashRed
indicatorBackground = R.drawable.background_indicator_outbound
} else {
lineOneColor = R.color.text_light_dimmed
lineTwoColor = R.color.text_light
}
}
raw == null || raw?.isEmpty() == true -> {
lineOne = "Unknown paid you"
toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
lineOne = getSender(transaction)
lineTwo = "Received $timestamp"
amountDisplay = "+ $amountZec"
amountColor = R.color.zcashGreen
@ -58,9 +74,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
else -> {
lineOne = "Unknown"
lineTwo = "Unknown"
amountDisplay = "$amountZec"
amountColor = R.color.text_light
}
}
// sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001"
else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
@ -73,17 +90,41 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
bottomText.text = lineTwo
amountText.text = amountDisplay
amountText.setTextColor(amountColor.toAppColor())
topText.setTextColor(lineOneColor.toAppColor())
bottomText.setTextColor(lineTwoColor.toAppColor())
val context = itemView.context
indicator.background = context.resources.getDrawable(indicatorBackground)
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded())
}
private fun getSender(transaction: ConfirmedTransaction): String {
val memo = transaction.memo.toUtf8Memo()
return when {
memo.contains(INCLUDE_MEMO_PREFIX) -> {
val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("eply to:") -> {
val address = memo.split("eply to:")[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("zs") -> {
val who = extractAddress(memo)?.toAbbreviatedAddress() ?: "Unknown"
"$who paid you"
}
else -> "Unknown paid you"
}
}
private fun extractAddress(memo: String?) =
addressRegex.findAll(memo ?: "").lastOrNull()?.value
private fun onTransactionClicked(transaction: ConfirmedTransaction) {
val txId = transaction.rawTransactionId.toTxId()
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
"Transaction: $txId" +
"${if (transaction.toAddress != null) "\n\nto: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nmemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"
"${if (transaction.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"
MaterialAlertDialogBuilder(itemView.context)
.setMessage(detailsMessage)
@ -98,6 +139,12 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
.show()
}
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
(transaction.toAddress ?: extractAddress(transaction.memo.toUtf8Memo()))?.let {
(itemView.context as MainActivity).copyText(it, "Transaction Address")
}
}
}
private fun ByteArray.toTxId(): String {

View File

@ -66,6 +66,7 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
adapter = TransactionAdapter()
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
binding.recyclerTransactions.adapter = adapter
binding.recyclerTransactions.smoothScrollToPosition(0)
}
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {

View File

@ -19,7 +19,8 @@ import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.Synchronizer.Status.*
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.convertZecToZatoshi
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
@ -92,7 +93,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
buttonNumberPadBack.asKey()
)
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) }
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) }
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) }
hitAreaScan.setOnClickListener {
mainActivity?.maybeOpenScan().also { tapped(HOME_SCAN) }
@ -195,8 +195,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
fun setProgress(uiModel: HomeViewModel.UiModel) {
if (!uiModel.processorInfo.hasData) {
twig("Warning: ignoring progress update because the processor has not started.")
if (!uiModel.processorInfo.hasData && !uiModel.isDisconnected) {
twig("Warning: ignoring progress update because the processor is still starting.")
return
}
@ -207,28 +207,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
val sendText = when {
uiModel.status == DISCONNECTED -> "Reconnecting . . ."
uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE"
uiModel.status == Synchronizer.Status.DISCONNECTED -> "DISCONNECTED"
uiModel.status == Synchronizer.Status.STOPPED -> "IDLE"
uiModel.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
// }
binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected)
}
/**
@ -243,10 +236,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) {
val availableString = if (availableBalance < 0) "Updating" else availableBalance.convertZatoshiToZecString()
val missingBalance = availableBalance < 0
val availableString = if (missingBalance) "Updating" else availableBalance.convertZatoshiToZecString()
binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
binding.labelBalance.transparentIf(missingBalance)
binding.textBalanceDescription.apply {
goneIf(availableBalance < 0)
goneIf(missingBalance)
text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
val change = (totalBalance - availableBalance).convertZatoshiToZecString()
"(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
@ -275,12 +271,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
logUpdate(old, new)
if (binding.lottieButtonLoading.visibility != View.VISIBLE) binding.lottieButtonLoading.visibility = View.VISIBLE
uiModel = new
if (old?.pendingSend != new.pendingSend) {
setSendAmount(new.pendingSend)
}
// TODO: handle stopped and disconnected flows
setProgress(uiModel) // TODO: we may not need to separate anymore
// if (new.status = SYNCING) onSyncing(new) else onSynced(new)
if (new.status == SYNCED) onSynced(new) else onSyncing(new)

View File

@ -21,8 +21,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
lateinit var uiModels: Flow<UiModel>
private val _typedChars = ConflatedBroadcastChannel<Char>()
private val typedChars = _typedChars.asFlow()
lateinit var _typedChars: ConflatedBroadcastChannel<Char>
var initialized = false
@ -32,12 +31,19 @@ class HomeViewModel @Inject constructor() : ViewModel() {
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
return
}
if (::_typedChars.isInitialized) {
_typedChars.close()
}
_typedChars = ConflatedBroadcastChannel()
val typedChars = _typedChars.asFlow()
val zec = typedChars.scan("0") { acc, c ->
when {
// no-op cases
acc == "0" && c == '0'
|| (c == '<' && acc == "0")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c $typedChars ")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c")
acc
}
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
@ -96,6 +102,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
val isDownloading = status == DOWNLOADING
val isScanning = status == SCANNING
val isValidating = status == VALIDATING
val isDisconnected = status == DISCONNECTED
val downloadProgress: Int get() {
return processorInfo.run {
if (lastDownloadRange.isEmpty()) {

View File

@ -7,7 +7,7 @@ import com.airbnb.lottie.LottieAnimationView
class MagicSnakeLoader(
val lottie: LottieAnimationView,
private val scanningStartFrame: Int = 100,
private val scanningEndFrame: Int = 175,
private val scanningEndFrame: Int = 187,
val totalFrames: Int = 200
) : ValueAnimator.AnimatorUpdateListener {
private var isPaused: Boolean = true

View File

@ -0,0 +1,92 @@
package cash.z.ecc.android.ui.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.Toast
import androidx.core.view.doOnLayout
import cash.z.ecc.android.databinding.FragmentFeedbackBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_CANCEL
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_SUBMIT
import cash.z.ecc.android.ui.base.BaseFragment
/**
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
* application.
*/
class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
override val screen = Report.Screen.FEEDBACK
override fun inflate(inflater: LayoutInflater): FragmentFeedbackBinding =
FragmentFeedbackBinding.inflate(inflater)
private lateinit var ratings: Array<View>
// private val padder = ViewTreeObserver.OnGlobalLayoutListener {
// Toast.makeText(mainActivity, "LAYOUT", Toast.LENGTH_SHORT).show()
// }
//
// LifeCycle
//
override fun onResume() {
super.onResume()
// mainActivity!!.window.decorView.viewTreeObserver.addOnGlobalLayoutListener(padder)
// mainActivity!!.findViewById<View>(android.R.id.content).viewTreeObserver.addOnGlobalLayoutListener(padder)
}
override fun onPause() {
super.onPause()
// mainActivity!!.window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(padder)
// mainActivity!!.findViewById<View>(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(padder)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
backButtonHitArea.setOnClickListener(::onFeedbackCancel)
buttonSubmit.setOnClickListener(::onFeedbackSubmit)
ratings = arrayOf(feedbackExp1, feedbackExp2, feedbackExp3, feedbackExp4, feedbackExp5)
ratings.forEach {
it.setOnClickListener(::onRatingClicked)
}
}
}
//
// Private API
//
private fun onFeedbackSubmit(view: View) {
Toast.makeText(mainActivity, "Thanks for the feedback!", Toast.LENGTH_LONG).show()
tapped(FEEDBACK_SUBMIT)
val q1 = binding.inputQuestion1.editText?.text.toString()
val q2 = binding.inputQuestion2.editText?.text.toString()
val q3 = binding.inputQuestion3.editText?.text.toString()
val rating = ratings.indexOfFirst { it.isActivated } + 1
mainActivity?.reportFunnel(UserFeedback.Submitted(rating, q1, q2, q3))
mainActivity?.navController?.navigateUp()
}
private fun onFeedbackCancel(view: View) {
tapped(FEEDBACK_CANCEL)
mainActivity?.reportFunnel(UserFeedback.Cancelled)
mainActivity?.navController?.navigateUp()
}
private fun onRatingClicked(view: View) {
ratings.forEach { it.isActivated = false }
view.isActivated = !view.isActivated
}
}

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.ui.profile
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@ -17,6 +16,7 @@ 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.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.toAbbreviatedAddress
@ -39,6 +39,11 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
super.onViewCreated(view, savedInstanceState)
binding.hitAreaClose.onClickNavBack() { tapped(PROFILE_CLOSE) }
binding.buttonBackup.onClickNavTo(R.id.action_nav_profile_to_nav_backup) { tapped(PROFILE_BACKUP) }
binding.buttonFeedback.onClickNavTo(R.id.action_nav_profile_to_nav_feedback) {
tapped(PROFILE_SEND_FEEDBACK)
mainActivity?.reportFunnel(UserFeedback.Started)
Unit
}
binding.textVersion.text = BuildConfig.VERSION_NAME
onClick(binding.buttonLogs) {
tapped(PROFILE_VIEW_USER_LOGS)
@ -49,10 +54,6 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
onViewDevLogs()
true
}
onClick(binding.buttonFeedback) {
tapped(PROFILE_SEND_FEEDBACK)
onSendFeedback()
}
}
override fun onResume() {
@ -95,10 +96,6 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
startActivity(Intent.createChooser(intent, "Share Log File"))
}
private fun onSendFeedback() {
mainActivity?.showSnackbar("Feedback feature coming soon!")
}
private fun userLogFile(): File? {
return mainActivity?.feedbackCoordinator?.findObserver<FeedbackFile>()?.file
}

View File

@ -54,7 +54,7 @@ class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Uni
private fun onImageScan(result: List<FirebaseVisionBarcode>, image: ImageProxy) {
result.firstOrNull()?.rawValue?.let {
scanCallback(it, image)
} ?: image.close()
} ?: runCatching { image.close() }
}
private fun onImageScanFailure(e: Exception) {

View File

@ -150,8 +150,8 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun onBalanceUpdated(balance: WalletBalance) {
binding.textLayoutAmount.helperText =
"You have ${balance.availableZatoshi.convertZatoshiToZecString(8)} available"
maxZatoshi = balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI
"You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available"
maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
}
override fun onPrimaryClipChanged() {

View File

@ -13,6 +13,7 @@ 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.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.entity.*
@ -51,7 +52,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
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" +
"Error: fromAddress was empty while attempting to include it in the memo. Verify" +
" that initFromAddress() has previously been called on this viewmodel."
}
field = value
@ -60,7 +61,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
fun send(): Flow<PendingTransaction> {
funnel(SendSelected)
val memoToSend = if (includeFromAddress) "$memo\nsent from\n$fromAddress" else memo
val memoToSend = createMemoToSend()
val keys = initializer.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
)
@ -76,6 +77,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX\n$fromAddress" else memo
private fun reportIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
@ -98,13 +101,16 @@ class SendViewModel @Inject constructor() : ViewModel() {
when {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit("Please enter a valid address")
emit("Please enter a valid address.")
}
zatoshiAmount < 1 -> {
emit("Too little! Please enter at least 1 Zatoshi.")
emit("Please enter at least 1 Zatoshi.")
}
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit( "Too much! Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}")
emit( "Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)} ZEC.")
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit( "Memo must be less than ${ZcashSdk.MAX_MEMO_SIZE} in length.")
}
else -> emit(null)
}
@ -191,7 +197,6 @@ class SendViewModel @Inject constructor() : ViewModel() {
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()
}

View File

@ -0,0 +1,28 @@
package cash.z.ecc.android.ui.util
import java.nio.charset.StandardCharsets
const val INCLUDE_MEMO_PREFIX = "sent from"
inline fun ByteArray?.toUtf8Memo(): String {
// TODO: make this more official but for now, this will do
return if (this == null || this[0] >= 0xF5) "" else try {
String(this, StandardCharsets.UTF_8).trim('\u0000')
} catch (t: Throwable) {
"unable to parse memo"
}
}
/*
if self.0[0] < 0xF5 {
// Check if it is valid UTF8
Some(str::from_utf8(&self.0).map(|memo| {
// Drop trailing zeroes
memo.trim_end_matches(char::from(0)).to_owned()
}))
} else {
None
}
*/

View File

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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/zcashWhite_12"
android:startColor="@color/zcashWhite_60" />
</shape>

View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/back_button"
app:layout_constraintStart_toStartOf="parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.9" />
<TextView
android:id="@+id/text_feedback_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please rank your experience"
android:textColor="@color/text_light"
android:textSize="22sp"
app:layout_constraintStart_toEndOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_feedback_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="We improve and iterate with YOUR feedback"
android:textColor="@color/text_light"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_feedback_title" />
<View
android:id="@+id/background_buttons"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:background="@color/zcashBlack_54"
app:layout_constraintTop_toBottomOf="@id/text_feedback_subtitle" />
<TextView
android:id="@+id/feedback_exp_1"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="1"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_2"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_2"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="2"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_3"
app:layout_constraintStart_toEndOf="@id/feedback_exp_1"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_3"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="3"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_4"
app:layout_constraintStart_toEndOf="@id/feedback_exp_2"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_4"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="4"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_5"
app:layout_constraintStart_toEndOf="@id/feedback_exp_3"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_5"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="5"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toEndOf="@id/feedback_exp_4"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/text_question_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_1"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/background_buttons" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_1"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_1">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_question_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_2"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_1" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_2"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_2">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_question_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_3"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_2" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_3"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_3">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_submit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
style="@style/Zcash.Button"
android:gravity="center"
android:padding="12dp"
android:text="Send Feedback"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_3" />
<Space
android:id="@+id/extra_padding_for_scrolling"
android:layout_width="match_parent"
android:layout_height="400dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_submit" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -241,18 +241,6 @@
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="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"
android:layout_width="match_parent"
@ -307,7 +295,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
android:tint="@color/colorAccent"
app:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.052"
@ -333,21 +321,6 @@
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_account_circle" />
<ImageView
android:id="@+id/icon_detail"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
android:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/text_detail"
app:layout_constraintEnd_toStartOf="@id/text_detail"
app:layout_constraintHeight_percent="0.044"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/text_detail"
app:layout_constraintWidth_percent="0.0887"
app:srcCompat="@drawable/ic_receipt_24dp" />
<View
android:id="@+id/hit_area_scan"
android:layout_width="68dp"
@ -376,13 +349,13 @@
android:padding="12dp"
android:elevation="6dp"
android:layout_marginTop="12dp"
android:text="Wallet Details"
android:text="Wallet History"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"
android:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon_detail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading" />
<TextView

View File

@ -223,7 +223,7 @@
android:id="@+id/group_transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:visibility="visible"
tools:visibility="gone"
app:constraint_referenced_ids="sad_description, sad_icon, sad_title, sad_checkbox" />
<androidx.constraintlayout.widget.Group
@ -231,5 +231,5 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="clear_memo, background_memo, input_memo, check_include_address, text_info_shielded"
tools:visibility="gone" />
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -65,7 +65,6 @@
android:id="@+id/nav_detail"
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
tools:layout="@layout/fragment_detail" />
<fragment
android:id="@+id/nav_profile"
android:name="cash.z.ecc.android.ui.profile.ProfileFragment"
@ -73,7 +72,14 @@
<action
android:id="@+id/action_nav_profile_to_nav_backup"
app:destination="@id/nav_backup" />
<action
android:id="@+id/action_nav_profile_to_nav_feedback"
app:destination="@id/nav_feedback" />
</fragment>
<fragment
android:id="@+id/nav_feedback"
android:name="cash.z.ecc.android.ui.profile.FeedbackFragment"
tools:layout="@layout/fragment_feedback" />
<!-- -->
<!-- Send Navigation -->

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="buttonCornerRadius">0dp</dimen>
<!-- Floats -->

View File

@ -8,7 +8,15 @@
<!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
<string name="send_hint_input_zcash_amount">Enter an amount to send</string>
<string name="send_memo_excluded_message">Your transaction is shielded and your address is not available to the recipient</string>
<string name="send_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</string>
<!-- Feedback -->
<string name="feedback_question_1">Any details you\'d like to share?</string>
<string name="feedback_question_2">Was your balance clear?</string>
<string name="feedback_question_3">What feature would you like to see next?</string>
<string name="feedback_hint_1">My experience was . . .</string>
<string name="feedback_hint_2">My balance was . . .</string>
<string name="feedback_hint_3">I\'d like . . .</string>
</resources>

View File

@ -10,6 +10,7 @@
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/Zcash.ShapeAppearance.MediumComponent</item>
</style>
<style name="ZcashTheme" parent="ZcashBaseTheme"/>
@ -67,17 +68,22 @@
<!-- Shape Appearances -->
<!-- General: buttons -->
<style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">20dp</item>
<item name="cornerSize">8dp</item>
</style>
<!-- General: dialogs -->
<style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
<item name="cornerSize">12dp</item>
</style>
<!-- Component -->
<style name="Zcash.ShapeAppearance.TextInputLayout" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
<item name="cornerSize">0dp</item>
</style>
<!-- Theme Overlays -->