Fixes after the team testing session.
This commit is contained in:
parent
fd5a0ff831
commit
62bbd30c40
|
@ -9,7 +9,7 @@ apply plugin: 'com.google.gms.google-services'
|
|||
|
||||
archivesBaseName = 'zcash-android-wallet'
|
||||
group = 'cash.z.ecc.android'
|
||||
version = '1.0.0-alpha05'
|
||||
version = '1.0.0-alpha06'
|
||||
|
||||
android {
|
||||
compileSdkVersion Deps.compileSdkVersion
|
||||
|
@ -19,7 +19,7 @@ android {
|
|||
applicationId 'cash.z.ecc.android'
|
||||
minSdkVersion Deps.minSdkVersion
|
||||
targetSdkVersion Deps.targetSdkVersion
|
||||
versionCode = 1_00_00_005
|
||||
versionCode = 1_00_00_006
|
||||
// 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'
|
||||
|
|
|
@ -12,7 +12,8 @@ import cash.z.ecc.android.feedback.FeedbackCoordinator
|
|||
import cash.z.wallet.sdk.ext.TroubleshootingTwig
|
||||
import cash.z.wallet.sdk.ext.Twig
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.squareup.okhttp.Dispatcher
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
@ -27,6 +28,15 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
|
|||
|
||||
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() {
|
||||
creationTime = System.currentTimeMillis()
|
||||
instance = this
|
||||
|
@ -35,6 +45,9 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
|
|||
|
||||
component = DaggerAppComponent.factory().create(this)
|
||||
component.inject(this)
|
||||
feedbackScope.launch {
|
||||
feedbackCoordinator.feedback.start()
|
||||
}
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(feedbackCoordinator, Thread.getDefaultUncaughtExceptionHandler()))
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -204,4 +210,39 @@ class MainActivity : AppCompatActivity() {
|
|||
if (!it.isShownOrQueued) it.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun maybeOpenScan() {
|
||||
if (hasCameraPermission) {
|
||||
openCamera()
|
||||
} 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() {
|
||||
navController.navigate(R.id.action_global_nav_scan)
|
||||
}
|
||||
|
||||
private fun onNoCamera() {
|
||||
showSnackbar("Well, this is awkward. You denied permission for the camera.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@ 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)
|
||||
text = "(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+${change.convertZatoshiToZecString()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,19 +3,18 @@ package cash.z.ecc.android.ui.home
|
|||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
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.ext.disabledIf
|
||||
import cash.z.ecc.android.ext.goneIf
|
||||
import cash.z.ecc.android.ext.onClickNavTo
|
||||
import cash.z.ecc.android.ext.toColoredSpan
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
|
||||
import cash.z.ecc.android.ui.send.SendViewModel
|
||||
|
@ -28,6 +27,7 @@ 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
|
||||
|
@ -92,7 +92,9 @@ 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()))
|
||||
|
@ -100,7 +102,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
buttonSend.setOnClickListener {
|
||||
onSend()
|
||||
}
|
||||
setSendAmount("0")
|
||||
setSendAmount("0", false)
|
||||
}
|
||||
|
||||
binding.buttonNumberPadBack.setOnLongClickListener {
|
||||
|
@ -115,9 +117,14 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
|
||||
private fun onClearAmount() {
|
||||
repeat(binding.textSendAmount.text.length) {
|
||||
if (::uiModel.isInitialized) {
|
||||
resumedScope.launch {
|
||||
_typedChars.send('<')
|
||||
binding.textSendAmount.text.apply {
|
||||
while (uiModel.pendingSend != "0") {
|
||||
_typedChars.send('<')
|
||||
delay(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,9 +191,11 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
/**
|
||||
* @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()
|
||||
if (updateModel) {
|
||||
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
|
||||
}
|
||||
binding.buttonSend.disabledIf(amount == "0")
|
||||
}
|
||||
|
||||
|
@ -197,7 +206,7 @@ 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)"
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
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,19 @@ 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.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 +22,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 +31,92 @@ 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 {
|
||||
onAddressChanged(text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
postDelayed({
|
||||
requestFocus()
|
||||
setSelection(text?.length ?: 0)
|
||||
}, 10L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
|
||||
|
@ -87,6 +130,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() {
|
||||
|
|
|
@ -8,6 +8,7 @@ import cash.z.wallet.sdk.Initializer
|
|||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
@ -41,7 +42,10 @@ 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 -> {
|
||||
|
@ -50,8 +54,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
|
|||
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> {
|
||||
emit("Please enter a larger amount")
|
||||
}
|
||||
synchronizer.getAddress() == toAddress -> {
|
||||
emit("That appears to be your address!")
|
||||
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
|
||||
emit( "Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}")
|
||||
}
|
||||
else -> emit(null)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -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,99 @@
|
|||
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"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_layout_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"
|
||||
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_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"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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 -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_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="@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="Amount"
|
||||
android:theme="@style/Zcash.Overlay.TextInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.84"/>
|
||||
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_constraintBottom_toTopOf="@id/text_banner_message"
|
||||
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 +138,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 +156,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 -->
|
||||
|
@ -147,18 +176,18 @@
|
|||
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_constraintStart_toStartOf="@+id/text_layout_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_next"
|
||||
app:layout_constraintVertical_bias="0.07" />
|
||||
|
||||
<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 +198,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>
|
|
@ -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"/>
|
||||
|
@ -157,4 +157,14 @@
|
|||
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" />
|
||||
|
||||
</navigation>
|
|
@ -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>
|
||||
|
|
|
@ -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,15 @@ 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."
|
||||
if(::scope.isInitialized) {
|
||||
val callStack = StringBuilder().let { s ->
|
||||
Thread.currentThread().stackTrace.forEach {element ->
|
||||
s.append(element.toString())
|
||||
}
|
||||
s.toString()
|
||||
}
|
||||
Log.e("@TWIG","Warning: did not initialize feedback because it has already been initialized. Call stack: $callStack")
|
||||
return this
|
||||
}
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob(coroutineContext[Job]))
|
||||
invokeOnCompletion {
|
||||
|
|
Loading…
Reference in New Issue