Fixes after the team testing session.

This commit is contained in:
Kevin Gorham 2020-01-10 02:53:16 -05:00
parent fd5a0ff831
commit 62bbd30c40
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
17 changed files with 363 additions and 95 deletions

View File

@ -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'

View File

@ -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())
}

View File

@ -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

View File

@ -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.")
}
}

View File

@ -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()}")
}
}

View File

@ -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)"
}

View File

@ -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) {

View File

@ -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!")
}
}

View File

@ -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()
}

View File

@ -14,6 +14,6 @@ class ReceiveViewModel @Inject constructor() : ViewModel() {
override fun onCleared() {
super.onCleared()
twig("WalletDetailViewModel cleared!")
twig("ReceiveViewModel cleared!")
}
}

View File

@ -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() {

View File

@ -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)
}

View File

@ -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
// }
// }
//}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {