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

396 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.presenter.Presenter
import cash.z.android.wallet.ui.presenter.SendPresenter
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import dagger.Binds
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
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
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
@Inject
2019-02-20 22:37:09 -08:00
lateinit var sendPresenter: SendPresenter
private lateinit var binding: FragmentSendBinding
2019-02-20 22:37:09 -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)
mainActivity?.setToolbarShown(true)
}
override fun onResume() {
super.onResume()
launch {
sendPresenter.start()
}
}
override fun onPause() {
super.onPause()
sendPresenter.stop()
}
//
// SendView Implementation
//
2019-02-20 22:37:09 -08:00
override fun exit() {
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
}
2019-02-20 22:37:09 -08:00
override fun setSendEnabled(isEnabled: Boolean) {
binding.buttonSendZec.isEnabled = isEnabled
}
//
// ScanFragment.BarcodeCallback implemenation
//
override fun onBarcodeScanned(value: String) {
exitScanMode()
binding.inputZcashAddress.setText(value)
2019-02-20 22:37:09 -08:00
sendPresenter.inputAddressUpdated(value)
}
//
// 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() {
2019-02-20 22:37:09 -08:00
/* Init - Text Input */
2019-01-14 00:09:02 -08:00
2019-02-16 00:47:39 -08:00
binding.textValueHeader.apply {
2019-02-20 22:37:09 -08:00
setSelectAllOnFocus(true)
afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputHeaderUpdating(it) }
doOnDoneOrFocusLost { sendPresenter.inputHeaderUpdated(it) }
}
binding.inputZcashAddress.apply {
afterTextChanged { if (it.isNotEmpty()) sendPresenter.inputAddressUpdating(it) }
doOnDoneOrFocusLost { sendPresenter.inputAddressUpdated(it) }
}
binding.textAreaMemo.apply {
2019-02-16 00:47:39 -08:00
afterTextChanged {
2019-02-20 22:37:09 -08:00
if (it.isNotEmpty()) sendPresenter.inputMemoUpdating(it)
binding.textMemoCharCount.text = "${text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
2019-01-14 00:09:02 -08:00
}
2019-02-20 22:37:09 -08:00
doOnDoneOrFocusLost { sendPresenter.inputMemoUpdated(it) }
2019-01-14 00:09:02 -08:00
}
2019-02-20 22:37:09 -08:00
/* Init - Taps */
2019-02-20 22:37:09 -08:00
binding.imageSwapCurrency.setOnClickListener {
// validate the amount before we toggle (or else we lose their uncommitted change)
sendPresenter.inputHeaderUpdated(binding.textValueHeader.text.toString())
sendPresenter.inputToggleCurrency()
}
binding.buttonSendZec.setOnClickListener{
exitScanMode()
sendPresenter.inputSendPressed()
}
2019-02-20 22:37:09 -08:00
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
sendPresenter.invalidate()
hideKeyboard()
2019-01-14 00:09:02 -08:00
}
2019-02-20 22:37:09 -08:00
/* Non-Presenter calls (UI-only logic) */
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
}
}
2019-02-20 22:37:09 -08:00
binding.dialogSendBackground.setOnClickListener { hideSendDialog() }
binding.dialogSubmitButton.setOnClickListener { onSendZec() }
binding.imageScanQr.setOnClickListener(::onScanQrCode)
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
}
2019-02-20 22:37:09 -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 in this class with production verstion getting an empty implementation that just hides the icon.
2019-02-16 00:47:39 -08:00
private fun onPasteShortcutAddress(view: View) {
view.context.alert(R.string.send_alert_shortcut_clicked) {
2019-02-20 22:37:09 -08:00
val address = SampleProperties.wallet.defaultSendAddress
binding.inputZcashAddress.setText(address)
sendPresenter.inputAddressUpdated(address)
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-02-20 22:37:09 -08:00
/**
* Called after confirmation dialog is affirmed. Begins the process of actually sending ZEC.
*/
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>()
2019-02-16 00:47:39 -08:00
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
2019-02-20 22:37:09 -08:00
checkAllInput()
2019-02-16 00:47:39 -08:00
}
private fun hideSendDialog() {
setSendEnabled(true)
binding.groupDialogSend.visibility = View.GONE
}
2019-02-20 22:37:09 -08:00
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
if (mainActivity != null) {
DrawableCompat.setTint(
binding.inputZcashAddress.background,
ContextCompat.getColor(mainActivity!!, colorRes)
)
}
2019-02-20 22:37:09 -08:00
}
/* Error handling */
override fun setAmountError(message: String?) {
if (message == null) {
binding.textValueError.visibility = View.GONE
binding.textValueError.text = null
} else {
binding.textValueError.text = message
binding.textValueError.visibility = View.VISIBLE
setSendEnabled(false)
}
}
2019-02-20 22:37:09 -08:00
override 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)
}
}
2019-02-20 22:37:09 -08:00
override fun setMemoError(message: String?) {
val validColor = R.color.zcashBlack_12.toAppColor()
val errorColor = R.color.zcashRed.toAppColor()
if (message == null) {
binding.dividerMemo.setBackgroundColor(validColor)
binding.textMemoCharCount.setTextColor(validColor)
binding.textAreaMemo.setTextColor(R.color.text_dark.toAppColor())
} else {
binding.dividerMemo.setBackgroundColor(errorColor)
binding.textMemoCharCount.setTextColor(errorColor)
binding.textAreaMemo.setTextColor(errorColor)
setSendEnabled(false)
}
}
/**
2019-02-20 22:37:09 -08:00
* Validate all input. This is essentially the same as extracting a model out of the view and validating it with the
* presenter. Basically, this needs to happen anytime something is edited, in order to try and enable Send. Right
* now this method is called 1) any time the model is updated with valid input, 2) anytime the keyboard is hidden,
* and 3) anytime send is pressed. It also triggers the only logic that can set "requiresValidation" to false.
*/
2019-02-20 22:37:09 -08:00
override fun checkAllInput(): Boolean {
with(binding) {
return sendPresenter.inputHeaderUpdated(textValueHeader.text.toString())
&& sendPresenter.inputAddressUpdated(inputZcashAddress.text.toString())
&& sendPresenter.inputMemoUpdated(textAreaMemo.text.toString())
}
}
// 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
@Binds
@Singleton
abstract fun providePresenter(sendPresenter: SendPresenter): Presenter
2018-11-12 10:38:37 -08:00
}