zcash-android-wallet-zcon1/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/SendFragment.kt

399 lines
14 KiB
Kotlin
Raw Normal View History

2018-11-12 10:38:37 -08:00
package cash.z.android.wallet.ui.fragment
2019-02-16 00:47:39 -08:00
import android.annotation.SuppressLint
2019-01-15 07:49:43 -08:00
import android.graphics.Typeface
2018-11-12 10:38:37 -08:00
import android.os.Bundle
2019-01-15 07:49:43 -08:00
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
2018-11-12 10:38:37 -08:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
2019-02-16 00:47:39 -08:00
import androidx.annotation.ColorRes
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
2019-02-16 00:47:39 -08:00
import androidx.core.graphics.drawable.DrawableCompat
2019-01-15 07:49:43 -08:00
import androidx.core.text.toSpannable
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
2019-02-16 00:47:39 -08:00
import cash.z.android.wallet.BuildConfig
2018-11-12 10:38:37 -08:00
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentSendBinding
2019-02-16 00:47:39 -08:00
import cash.z.android.wallet.extention.*
2019-02-14 17:26:56 -08:00
import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.presenter.SendPresenter
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
2019-01-14 00:09:02 -08:00
import java.text.DecimalFormat
2019-02-16 00:47:39 -08:00
import kotlin.math.absoluteValue
2018-11-12 10:38:37 -08:00
/**
* Fragment for sending Zcash.
2018-11-12 10:38:37 -08:00
*
*/
class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback {
2018-11-12 10:38:37 -08:00
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
2019-02-16 00:47:39 -08:00
private val zec = R.string.zec_abbreviation.toAppString()
private val usd = R.string.usd_abbreviation.toAppString()
2019-01-14 00:09:02 -08:00
//
// Lifecycle
//
2018-11-12 10:38:37 -08:00
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return DataBindingUtil.inflate<FragmentSendBinding>(
inflater, R.layout.fragment_send, container, false
).let {
binding = it
it.root
}
2018-11-12 10:38:37 -08:00
}
override fun onAttachFragment(childFragment: Fragment?) {
super.onAttachFragment(childFragment)
(childFragment as? ScanFragment)?.barcodeCallback = this
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2019-02-16 00:47:39 -08:00
init()
2019-01-14 00:09:02 -08:00
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sendPresenter = SendPresenter(this, mainActivity.synchronizer)
}
override fun onResume() {
super.onResume()
launch {
sendPresenter.start()
}
}
override fun onPause() {
super.onPause()
sendPresenter.stop()
}
//
// SendView Implementation
//
override fun submit() {
mainActivity.navController.navigate(R.id.nav_home_fragment)
}
override fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String) {
showCurrencySymbols(isUsdSelected)
setHeaderValue(headerString)
setSubheaderValue(subheaderString, isUsdSelected)
}
override fun setHeaderValue(value: String) {
binding.textValueHeader.setText(value)
}
@SuppressLint("SetTextI18n") // SetTextI18n lint logic has errors and does not recognize that the entire string contains variables, formatted per locale and loaded from string resources.
override fun setSubheaderValue(value: String, isUsdSelected: Boolean) {
val subheaderLabel = if (isUsdSelected) zec else usd
binding.textValueSubheader.text = "$value $subheaderLabel" //ignore SetTextI18n error here because it is invalid
}
override fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean) {
hideKeyboard()
setSendEnabled(false) // partially because we need to lower the button elevation
binding.dialogTextTitle.text = getString(R.string.send_dialog_title, zecString, zec, usdString)
binding.dialogTextAddress.text = toAddress
binding.dialogTextMemoIncluded.visibility = if(hasMemo) View.VISIBLE else View.GONE
binding.groupDialogSend.visibility = View.VISIBLE
}
override fun updateBalance(new: Long) {
// TODO: use a formatted string resource here
val availableTextSpan = "${new.convertZatoshiToZecString(8)} $zec Available".toSpannable()
availableTextSpan.setSpan(ForegroundColorSpan(R.color.colorPrimary.toAppColor()), availableTextSpan.length - "Available".length, availableTextSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
availableTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.textZecValueAvailable.text = availableTextSpan
}
//
// ScanFragment.BarcodeCallback implemenation
//
override fun onBarcodeScanned(value: String) {
exitScanMode()
binding.inputZcashAddress.setText(value)
validateAddressInput()
}
//
// Internal View Logic
//
/**
* Initialize view logic only. Click listeners, text change handlers and tooltips.
*/
2019-02-16 00:47:39 -08:00
private fun init() {
/* Presenter calls */
binding.imageSwapCurrency.setOnClickListener {
sendPresenter.toggleCurrency()
2019-01-14 00:09:02 -08:00
}
2019-02-16 00:47:39 -08:00
binding.textValueHeader.apply {
afterTextChanged {
sendPresenter.headerUpdating(it)
2019-01-14 00:09:02 -08:00
}
}
binding.buttonSendZec.setOnClickListener {
sendPresenter.sendPressed()
}
/* Non-Presenter calls (UI-only logic) */
binding.textAreaMemo.afterTextChanged {
2019-02-16 00:47:39 -08:00
binding.textMemoCharCount.text =
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
2019-01-14 00:09:02 -08:00
}
binding.imageScanQr.apply {
2019-02-16 00:47:39 -08:00
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
}
2019-02-16 00:47:39 -08:00
binding.imageAddressShortcut?.apply {
if (BuildConfig.DEBUG) {
visibility = View.VISIBLE
2019-02-16 00:47:39 -08:00
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
setOnClickListener(::onPasteShortcutAddress)
} else {
visibility = View.GONE
}
}
binding.dialogSendBackground.setOnClickListener {
hideSendDialog()
}
binding.dialogSubmitButton.setOnClickListener {
onSendZec()
}
2019-02-14 17:26:56 -08:00
binding.imageScanQr.setOnClickListener(::onScanQrCode)
2019-02-04 11:11:22 -08:00
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
it?.findFocus()?.clearFocus()
validateUserInput()
hideKeyboard()
}
2019-02-01 08:10:43 -08:00
binding.buttonSendZec.text = getString(R.string.send_button_label, zec)
setSendEnabled(false)
}
private fun showCurrencySymbols(isUsdSelected: Boolean) {
// visibility has some kind of bug that appears to be related to layout groups. So using alpha instead since our API level is high enough to support that
if (isUsdSelected) {
binding.textDollarSymbolHeader.alpha = 1.0f
binding.imageZecSymbolSubheader.alpha = 1.0f
binding.imageZecSymbolHeader.alpha = 0.0f
binding.textDollarSymbolSubheader.alpha = 0.0f
2019-02-16 00:47:39 -08:00
} else {
binding.imageZecSymbolHeader.alpha = 1.0f
binding.textDollarSymbolSubheader.alpha = 1.0f
binding.textDollarSymbolHeader.alpha = 0.0f
binding.imageZecSymbolSubheader.alpha = 0.0f
2019-02-16 00:47:39 -08:00
}
}
private fun onScanQrCode(view: View) {
hideKeyboard()
val fragment = ScanFragment()
val ft = childFragmentManager.beginTransaction()
.add(R.id.camera_placeholder, fragment, "camera_fragment")
.addToBackStack("camera_fragment_scanning")
2019-02-16 00:47:39 -08:00
.commit()
binding.groupHiddenDuringScan.visibility = View.INVISIBLE
binding.buttonCancelScan.apply {
visibility = View.VISIBLE
animate().alpha(1.0f).apply {
duration = 3000L
}
setOnClickListener {
exitScanMode()
}
}
2019-02-16 00:47:39 -08:00
}
// TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder here.
private fun onPasteShortcutAddress(view: View) {
view.context.alert(R.string.send_alert_shortcut_clicked) {
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
validateAddressInput()
2019-02-16 00:47:39 -08:00
hideKeyboard()
2019-01-14 00:09:02 -08:00
}
}
2018-11-12 10:38:37 -08:00
2019-01-14 00:09:02 -08:00
private fun onSendZec() {
setSendEnabled(false)
sendPresenter.sendFunds()
2019-01-14 00:09:02 -08:00
}
private fun exitScanMode() {
val cameraFragment = childFragmentManager.findFragmentByTag("camera_fragment")
if (cameraFragment != null) {
val ft = childFragmentManager.beginTransaction()
.remove(cameraFragment)
.commit()
}
binding.buttonCancelScan.visibility = View.GONE
binding.groupHiddenDuringScan.visibility = View.VISIBLE
}
2019-02-16 00:47:39 -08:00
private fun hideKeyboard() {
mainActivity.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}
private fun hideSendDialog() {
setSendEnabled(true)
binding.groupDialogSend.visibility = View.GONE
}
// note: be careful calling this with `true` that should only happen when all conditions have been validated
private fun setSendEnabled(isEnabled: Boolean) {
binding.buttonSendZec.isEnabled = isEnabled
}
private fun setAddressError(message: String?) {
if (message == null) {
setAddressLineColor()
binding.textAddressError.text = null
binding.textAddressError.visibility = View.GONE
} else {
setAddressLineColor(R.color.zcashRed)
binding.textAddressError.text = message
binding.textAddressError.visibility = View.VISIBLE
setSendEnabled(false)
}
}
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
DrawableCompat.setTint(
binding.inputZcashAddress.background,
ContextCompat.getColor(mainActivity, colorRes)
)
}
private fun setAmountError(isError: Boolean) {
val color = if (isError) R.color.zcashRed else R.color.text_dark
binding.textAmountBackground.setTextColor(color.toAppColor())
}
//
// Validation
//
override fun validateUserInput(): Boolean {
val allValid = validateAddressInput() && validateAmountInput() && validateMemo()
setSendEnabled(allValid)
return allValid
}
/**
* Validate the memo input and update presenter when valid.
*
* @return true when the memo is valid
*/
private fun validateMemo(): Boolean {
val memo = binding.textAreaMemo.text.toString()
return memo.all { it.isLetterOrDigit() }.also { if (it) sendPresenter.memoValidated(memo) }
}
/**
* Validate the address input and update presenter when valid.
*
* @return true when the address is valid
*/
private fun validateAddressInput(): Boolean {
var isValid = false
val address = binding.inputZcashAddress.text.toString()
if (address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
else if (address.any { !it.isLetterOrDigit() }) setAddressError(R.string.send_error_address_invalid_char.toAppString())
else setAddressError(null).also { isValid = true; sendPresenter.addressValidated(address) }
return isValid
}
/**
* Validate the amount input and update the presenter when valid.
*
* @return true when the amount is valid
*/
private fun validateAmountInput(): Boolean {
return try {
val amount = binding.textValueHeader.text.toString().safelyConvertToBigDecimal()!!
sendPresenter.headerValidated(amount)
setAmountError(false)
true
} catch (t: Throwable) {
Toaster.short("Invalid ZEC or USD value")
setSendEnabled(false)
setAmountError(true)
false
}
}
// TODO: come back to this test code later and fix the shared element transitions
//
// fun submitWithSharedElements() {
// var extras = with(binding) {
// listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
// .map{ it to it.transitionName }
// .let { FragmentNavigatorExtras(*it.toTypedArray()) }
// }
// val extras = FragmentNavigatorExtras(
// binding.dialogSendContents to binding.dialogSendContents.transitionName,
// binding.dialogTextTitle to getString(R.string.transition_active_transaction_title),
// binding.dialogTextAddress to getString(R.string.transition_active_transaction_address),
// binding.dialogSendBackground to getString(R.string.transition_active_transaction_background)
// )
//
// mainActivity.navController.navigate(R.id.nav_home_fragment,
// null,
// null,
// extras)
// }
}
@Module
abstract class SendFragmentModule {
@ContributesAndroidInjector
abstract fun contributeSendFragment(): SendFragment
2018-11-12 10:38:37 -08:00
}