checkpoint: completed major functionality in send including refactor

added CameraView and wired it into a fragment, then connected it with firebasevision. Also completely cleaned up the sendfragment and refactored the way that it manages currency conversions.
This commit is contained in:
Kevin Gorham 2019-02-18 00:05:40 -05:00 committed by Kevin Gorham
parent e26d66cc3b
commit 92b9558446
17 changed files with 862 additions and 386 deletions

View File

@ -106,7 +106,13 @@ dependencies {
debugImplementation deps.stetho
mockImplementation deps.stetho
testImplementation deps.junit
testImplementation 'org.mockito:mockito-junit-jupiter:2.24.0'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.0"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.4.0"
testImplementation "org.junit.jupiter:junit-jupiter-migrationsupport:5.4.0"
androidTestImplementation deps.androidx.test.runner
androidTestImplementation deps.androidx.test.espresso
}

View File

@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<dist:module dist:instant="true" />

View File

@ -1,6 +1,8 @@
package cash.z.android.wallet.sample
import cash.z.wallet.sdk.data.SampleSeedProvider
import java.math.BigDecimal
import java.math.MathContext
object AliceWallet {
const val name = "test.reference.alice"
@ -45,11 +47,5 @@ object SampleProperties {
const val COMPACT_BLOCK_PORT = 9067
val wallet = AliceWallet
// TODO: placeholder until we have a network service for this
const val USD_PER_ZEC = 49.07
/**
* A simple flag that helps with removing shortcuts in the code used during development.
* TODO: either elevate this to a real thing (based off a system property or some such) or delete it!
*/
const val DEV_MODE = false
val USD_PER_ZEC = BigDecimal("49.07", MathContext.DECIMAL128)
}

View File

@ -20,7 +20,6 @@ import androidx.navigation.ui.setupWithNavController
import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.R
import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.android.support.DaggerAppCompatActivity
@ -49,12 +48,12 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(!DEV_MODE)synchronizer.start(this)
synchronizer.start(this)
}
override fun onDestroy() {
super.onDestroy()
if(!DEV_MODE)synchronizer.stop()
synchronizer.stop()
}
override fun onBackPressed() {

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.android.wallet.R
import cash.z.android.wallet.extention.toAppColor
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.ext.convertZatoshiToZec
import cash.z.wallet.sdk.ext.toZec
import java.text.SimpleDateFormat
import java.util.*
@ -40,7 +41,7 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
val sign = if(tx.isSend) "-" else "+"
val amountColor = if (tx.isSend) R.color.text_dark_dimmed else R.color.colorPrimary
val transactionColor = if(tx.isSend) R.color.send_associated else R.color.receive_associated
val zecAbsoluteValue = tx.value.absoluteValue.toZec(3)
val zecAbsoluteValue = tx.value.absoluteValue.convertZatoshiToZec(3)
status.setBackgroundColor(transactionColor.toAppColor())
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending" else formatter.format(tx.timeInSeconds * 1000)
Log.e("TWIG-z", "TimeInSeconds: ${tx.timeInSeconds}")

View File

@ -23,7 +23,6 @@ import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentHomeBinding
import cash.z.android.wallet.extention.*
import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
import cash.z.android.wallet.ui.adapter.TransactionAdapter
import cash.z.android.wallet.ui.presenter.HomePresenter
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
@ -33,7 +32,7 @@ import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.data.ActiveSendTransaction
import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.wallet.sdk.data.TransactionState
import cash.z.wallet.sdk.ext.toZec
import cash.z.wallet.sdk.ext.*
import com.google.android.material.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem
import dagger.Module
@ -168,11 +167,11 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
//TODO: pull some of this logic into the presenter, particularly the part that deals with ZEC <-> USD price conversion
override fun updateBalance(old: Long, new: Long) {
val zecValue = new/1e8
setZecValue(zecValue)
setUsdValue(SampleProperties.USD_PER_ZEC * zecValue)
val zecValue = new.convertZatoshiToZec()
setZecValue(zecValue.toZecString(3))
setUsdValue(zecValue.convertZecToUsd(SampleProperties.USD_PER_ZEC).toUsdString())
onContentRefreshComplete(zecValue)
onContentRefreshComplete(new)
}
override fun setTransactions(transactions: List<WalletTransaction>) {
@ -245,12 +244,12 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
setActiveTransactionsShown(true)
Log.e("TWIG", "setting transaction state to ${transactionState::class.simpleName}")
var title = "Active Transaction"
var subtitle = "Processing..."
var title = binding.includeContent.textActiveTransactionTitle.text?.toString() ?: ""
var subtitle = binding.includeContent.textActiveTransactionSubtitle.text?.toString() ?: ""
when (transactionState) {
TransactionState.Creating -> {
binding.includeContent.headerActiveTransaction.visibility = View.VISIBLE
title = "Preparing ${transaction.value.toZec(3)} ZEC"
title = "Preparing ${transaction.value.convertZatoshiToZecString(3)} ZEC"
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}"
setTransactionActive(transaction, true)
}
@ -281,7 +280,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
binding.includeContent.lottieActiveTransaction.playAnimation()
title = "ZEC Sent"
subtitle = "Today at 2:11pm"
binding.includeContent.textActiveTransactionValue.text = transaction.value.toZec(3).toString()
binding.includeContent.textActiveTransactionValue.text = transaction.value.convertZatoshiToZecString(3)
binding.includeContent.textActiveTransactionValue.visibility = View.VISIBLE
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
} else {
@ -405,8 +404,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
.create()
}
private fun setUsdValue(value: Double) {
val valueString = String.format("$%,.2f",value)
private fun setUsdValue(valueString: String) {
val hairSpace = "\u200A"
// val adjustedValue = "$$hairSpace$valueString"
val textSpan = SpannableString(valueString)
@ -415,8 +413,8 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
binding.includeHeader.textBalanceUsd.text = textSpan
}
private fun setZecValue(value: Double) {
binding.includeHeader.textBalanceZec.text = if(value == 0.0) "0" else String.format("%.3f",value)
private fun setZecValue(value: String) {
binding.includeHeader.textBalanceZec.text = value
// // bugfix: there is a bug in motionlayout that causes text to flicker as it is resized because the last character doesn't fit. Padding both sides with a thin space works around this bug.
@ -431,8 +429,8 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
* If the balance changes from zero, the wallet is no longer empty so hide the empty view.
* But don't do either of these things if the situation has not changed.
*/
private fun onContentRefreshComplete(value: Double) {
val isEmpty = value <= 0.0
private fun onContentRefreshComplete(value: Long) {
val isEmpty = value <= 0L
// wasEmpty isn't enough info. it must be considered along with whether these views were ever initialized
val wasEmpty = binding.includeContent.groupEmptyViewItems.visibility == View.VISIBLE
// situation has changed when we weren't initialized but now we have a balance or emptiness has changed
@ -537,42 +535,15 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
lateinit var headerEmptyViews: Array<View>
lateinit var headerFullViews: Array<View>
fun shrink(): Double {
return binding.includeHeader.textBalanceZec.text.toString().trim().toDouble() - Random.nextDouble(5.0)
}
fun grow(): Double {
return binding.includeHeader.textBalanceZec.text.toString().trim().toDouble() + Random.nextDouble(5.0)
}
fun reduceValue() {
shrink().let {
if(it < 0) { setZecValue(0.0); toggleViews(empty); forceRedraw() }
else view?.postDelayed({
setZecValue(it)
setUsdValue(it*75.0)
reduceValue()
}, delay)
}
}
fun increaseValue(target: Double) {
grow().let {
if(it > target) { setZecValue(target); setUsdValue(target*75.0); toggleViews(empty) }
else view?.postDelayed({
setZecValue(it)
setUsdValue(it*75.0)
increaseValue(target)
if (headerFullViews[0].parent == null || headerEmptyViews[0].parent != null) toggleViews(false)
forceRedraw()
}, delay)
}
}
fun forceRedraw() {
view?.postDelayed({
binding.includeHeader.containerHomeHeader.progress = binding.includeHeader.containerHomeHeader.progress - 0.1f
}, delay * 2)
}
internal fun toggle(isEmpty: Boolean) {
toggleValues(isEmpty)
}
// internal fun toggle(isEmpty: Boolean) {
// toggleValues(isEmpty)
// }
internal fun toggleViews(isEmpty: Boolean) {
Log.e("TWIG-t", "toggling views to isEmpty == $isEmpty")
@ -643,14 +614,14 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
view?.postDelayed(::forceRedraw, delay * 2)
}
internal fun toggleValues(isEmpty: Boolean) {
empty = isEmpty
if(empty) {
reduceValue()
} else {
increaseValue(Random.nextDouble(20.0, 100.0))
}
}
// internal fun toggleValues(isEmpty: Boolean) {
// empty = isEmpty
// if(empty) {
// reduceValue()
// } else {
// increaseValue(Random.nextDouble(20.0, 100.0))
// }
// }
inner class HomeTransitionListener : Transition.TransitionListener {

View File

@ -1,27 +1,67 @@
package cash.z.android.wallet.ui.fragment
import android.animation.Animator
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import cash.z.android.cameraview.CameraView
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentScanBinding
import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.ui.activity.MainActivity
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import dagger.Module
import dagger.android.ContributesAndroidInjector
/**
* Fragment for scanning addresss, hopefully.
*/
class ScanFragment : BaseFragment() {
lateinit var binding: FragmentScanBinding
var barcodeCallback: BarcodeCallback? = null
// private var cameraSource: CameraSource? = null
interface BarcodeCallback {
fun onBarcodeScanned(value: String)
}
private val revealCamera = Runnable {
binding.overlayBarcodeScan.apply {
val cX = measuredWidth / 2
val cY = measuredHeight / 2
ViewAnimationUtils.createCircularReveal(this, cX, cY, 0.0f, cX.toFloat()).start()
postDelayed({
val v:View = this
v.animate().alpha(0.0f).apply { duration = 2400L }.setListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
binding.overlayBarcodeScan.visibility = View.GONE
}
override fun onAnimationCancel(animation: Animator?) {
binding.overlayBarcodeScan.visibility = View.GONE
}
})
},500L)
}
}
private val requiredPermissions: Array<String?>
get() {
@ -77,6 +117,8 @@ class ScanFragment : BaseFragment() {
override fun onResume() {
super.onResume()
binding.overlayBarcodeScan.post(revealCamera)
System.err.println("camoorah : onResume ScanFragment")
if(allPermissionsGranted()) onStartCamera()
// launch {
// sendPresenter.start()
@ -161,9 +203,62 @@ class ScanFragment : BaseFragment() {
private fun onStartCamera() {
with(binding.cameraView) {
// workaround race conditions with google play services downloading the binaries for Firebase Vision APIs
postDelayed({
firebaseCallback = PoCallback()
start()
}, 1500L)
}, 1000L)
}
}
inner class PoCallback : CameraView.FirebaseCallback {
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
.setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
.build()
val barcodeDetector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)
var cameraId = getBackCameraId()
private fun getBackCameraId(): String {
val manager = mainActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
for (cameraId in manager.cameraIdList) {
val characteristics = manager.getCameraCharacteristics(cameraId)
val cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING)!!
if (cOrientation == CameraCharacteristics.LENS_FACING_BACK) return cameraId
}
throw IllegalArgumentException("no rear-facing camera found!")
}
override fun onImageAvailable(image: Image) {
System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
var firebaseImage = FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity))
barcodeDetector
.detectInImage(firebaseImage)
.addOnSuccessListener { results ->
if (results.isNotEmpty()) {
val barcode = results[0]
val value = barcode.rawValue
val message = "found: $value"
Toaster.short(message)
onScanSuccess(value!!)
// TODO: highlight the barcode
var bounds = barcode.boundingBox
var corners = barcode.cornerPoints
binding.cameraView.setBarcode(barcode)
}
}
}
}
private var pendingSuccess = false
private fun onScanSuccess(value: String) {
if (!pendingSuccess) {
pendingSuccess = true
with(binding.cameraView) {
postDelayed({
barcodeCallback?.onBarcodeScanned(value)
}, 3000L)
}
}
}
@ -177,7 +272,6 @@ class ScanFragment : BaseFragment() {
}
}
@Module
abstract class ScanFragmentModule {
@ContributesAndroidInjector

View File

@ -17,56 +17,43 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.text.toSpannable
import androidx.databinding.DataBindingUtil
import androidx.navigation.fragment.FragmentNavigatorExtras
import cash.z.android.qrecycler.QScanner
import androidx.fragment.app.Fragment
import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentSendBinding
import cash.z.android.wallet.extention.*
import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
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
import java.text.DecimalFormat
import javax.inject.Inject
import kotlin.math.absoluteValue
/**
* Fragment for sending Zcash.
*
*/
class SendFragment : BaseFragment(), SendPresenter.SendView {
class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback {
@Inject
lateinit var qrCodeScanner: QScanner
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
private val zecFormatter = DecimalFormat("#.######")
private val usdFormatter = DecimalFormat("###,###,##0.00")
private val usdSelected get() = binding.groupUsdSelected.visibility == View.VISIBLE
private val zec = R.string.zec_abbreviation.toAppString()
private val usd = R.string.usd_abbreviation.toAppString()
//
// Lifecycle
//
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// val enterTransitionSet = TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
// duration = 3500L
// }
//
// this.sharedElementReturnTransition = enterTransitionSet
// this.sharedElementEnterTransition = enterTransitionSet
//
// this.allowReturnTransitionOverlap = false
// allowEnterTransitionOverlap = false
return DataBindingUtil.inflate<FragmentSendBinding>(
inflater, R.layout.fragment_send, container, false
).let {
@ -75,6 +62,11 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
}
}
override fun onAttachFragment(childFragment: Fragment?) {
super.onAttachFragment(childFragment)
(childFragment as? ScanFragment)?.barcodeCallback = this
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).let { mainActivity ->
@ -83,119 +75,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
}
init()
initDialog()
}
// temporary function until presenter is setup
private fun init() {
binding.imageSwapCurrency.setOnClickListener {
onToggleCurrency()
}
binding.textValueHeader.apply {
afterTextChanged {
tryIgnore {
// only update things if the user is actively editing. in other words, don't update on programmatic changes
if (binding.textValueHeader.hasFocus()) {
val value = binding.textValueHeader.text.toString().toDouble()
binding.textValueSubheader.text = if (usdSelected) {
zecFormatter.format(value / SampleProperties.USD_PER_ZEC) + " $zec"
} else {
if (value == 0.0) "0 $usd"
else usdFormatter.format(value * SampleProperties.USD_PER_ZEC) + " $usd"
}
}
}
}
}
binding.textAreaMemo.afterTextChanged {
binding.textMemoCharCount.text =
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
}
binding.buttonSendZec.setOnClickListener {
showSendDialog()
}
binding.buttonSendZec.isEnabled = false
with(binding.imageScanQr) {
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
}
binding.imageAddressShortcut?.apply {
if (BuildConfig.DEBUG) {
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
setOnClickListener(::onPasteShortcutAddress)
} else {
visibility = View.GONE
}
}
binding.imageScanQr.setOnClickListener(::onScanQrCode)
binding.textValueHeader.setText("0")
binding.textValueSubheader.text =
mainActivity.resources.getString(R.string.send_subheader_value, if (usdSelected) zec else usd)
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
it?.findFocus()?.clearFocus()
formatUserInput()
hideKeyboard()
}
setSendEnabled(true)
onToggleCurrency()
}
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
DrawableCompat.setTint(
binding.inputZcashAddress.background,
ContextCompat.getColor(mainActivity, colorRes)
)
}
fun formatUserInput() {
formatAmountInput()
formatAddressInput()
}
private fun formatAmountInput() {
val value = binding.textValueHeader.text.toString().toDouble().absoluteValue
binding.textValueHeader.setText(
when {
value == 0.0 -> "0"
usdSelected -> usdFormatter.format(value)
else -> zecFormatter.format(value)
}
)
}
private fun formatAddressInput() {
val address = binding.inputZcashAddress.text
if(address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
else setAddressError(null)
}
private fun setAddressError(message: String?) {
if (message == null) {
setAddressLineColor()
binding.textAddressError.text = null
binding.textAddressError.visibility = View.GONE
binding.buttonSendZec.isEnabled = true
} else {
setAddressLineColor(R.color.zcashRed)
binding.textAddressError.text = message
binding.textAddressError.visibility = View.VISIBLE
binding.buttonSendZec.isEnabled = false
}
}
private fun initDialog() {
binding.dialogSendBackground.setOnClickListener {
hideSendDialog()
}
binding.dialogSubmitButton.setOnClickListener {
if (DEV_MODE) submit() else onSendZec()
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -208,7 +87,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
launch {
sendPresenter.start()
}
if(DEV_MODE) showSendDialog()
}
override fun onPause() {
@ -216,56 +94,137 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
sendPresenter.stop()
}
//
// SendView Implementation
//
override fun submit() {
submitNoAnimations()
mainActivity.navController.navigate(R.id.nav_home_fragment)
}
private fun submitNoAnimations() {
mainActivity.navController.navigate(
R.id.nav_home_fragment,
null,
null,
FragmentNavigatorExtras(binding.dialogTextTitle to "transition_active_transaction_title")
)
override fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String) {
showCurrencySymbols(isUsdSelected)
setHeaderValue(headerString)
setSubheaderValue(subheaderString, isUsdSelected)
}
fun submitWithSharedElements() {
var extras = with(binding) {
listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
.map{ it to it.transitionName }
.let { FragmentNavigatorExtras(*it.toTypedArray()) }
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(old: Long, 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.
*/
private fun init() {
/* Presenter calls */
binding.imageSwapCurrency.setOnClickListener {
sendPresenter.toggleCurrency()
}
// 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)
binding.textValueHeader.apply {
afterTextChanged {
sendPresenter.headerUpdating(it)
}
}
binding.buttonSendZec.setOnClickListener {
sendPresenter.sendPressed()
}
/* Non-Presenter calls (UI-only logic) */
binding.textAreaMemo.afterTextChanged {
binding.textMemoCharCount.text =
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
}
binding.imageScanQr.apply {
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
}
binding.imageAddressShortcut?.apply {
if (BuildConfig.DEBUG) {
visibility = View.VISIBLE
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()
}
binding.imageScanQr.setOnClickListener(::onScanQrCode)
// allow background taps to dismiss the keyboard and clear focus
binding.contentFragmentSend.setOnClickListener {
it?.findFocus()?.clearFocus()
validateUserInput()
hideKeyboard()
}
binding.buttonSendZec.text = getString(R.string.send_button_label, zec)
setSendEnabled(false)
}
@SuppressLint("SetTextI18n")
fun onToggleCurrency() {
view?.findFocus()?.clearFocus()
formatUserInput()
val isInitiallyUsd = usdSelected // hold this value because we modify visibility here and that's what the value is based on
val subHeaderValue = binding.textValueSubheader.text.toString().substringBefore(' ')
val currencyLabelAfterToggle = if (isInitiallyUsd) usd else zec // what is selected is about to move to the subheader where the currency is labelled
binding.textValueSubheader.post {
binding.textValueSubheader.text = "${binding.textValueHeader.text} $currencyLabelAfterToggle"
binding.textValueHeader.setText(subHeaderValue)
}
if (isInitiallyUsd) {
binding.groupZecSelected.visibility = View.VISIBLE
binding.groupUsdSelected.visibility = View.GONE
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
} else {
binding.groupZecSelected.visibility = View.GONE
binding.groupUsdSelected.visibility = View.VISIBLE
binding.imageZecSymbolHeader.alpha = 1.0f
binding.textDollarSymbolSubheader.alpha = 1.0f
binding.textDollarSymbolHeader.alpha = 0.0f
binding.imageZecSymbolSubheader.alpha = 0.0f
}
}
@ -274,72 +233,44 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
val fragment = ScanFragment()
val ft = childFragmentManager.beginTransaction()
.add(R.id.camera_placeholder, fragment, "camera_fragment")
.addToBackStack("camera_fragment_scanning")
.commit()
// val intent = Intent(mainActivity, CameraQrScanner::class.java)
// mainActivity.startActivity(intent)
// qrCodeScanner.scanBarcode { barcode: Result<String> ->
// if (barcode.isSuccess) {
// binding.inputZcashAddress.setText(barcode.getOrThrow())
// formatAddressInput()
// } else {
// Toaster.short("failed to scan QR code")
// }
// }
binding.groupHiddenDuringScan.visibility = View.INVISIBLE
binding.buttonCancelScan.apply {
visibility = View.VISIBLE
animate().alpha(1.0f).apply {
duration = 3000L
}
setOnClickListener {
exitScanMode()
}
}
}
// 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)
setAddressError(null)
validateAddressInput()
hideKeyboard()
}
}
override fun updateBalance(old: Long, new: Long) {
val zecBalance = new / 100000000.0
val usdBalance = zecBalance * SampleProperties.USD_PER_ZEC
val availableZecFormatter = DecimalFormat("#.########")
// TODO: use a formatted string resource here
val availableTextSpan = "${availableZecFormatter.format(zecBalance)} $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
}
private fun onSendZec() {
setSendEnabled(false)
// val currency = if(zecSelected) "ZEC" else "USD"
// Toaster.short("sending ${text_value_header.text} $currency...")
//TODO: convert and use only zec amount
// val amount = text_value_header.text.toString().toDouble()
// val address = input_zcash_address.text.toString()
val amount = 0.0018
val address = "ztestsapling1fg82ar8y8whjfd52l0xcq0w3n7nn7cask2scp9rp27njeurr72ychvud57s9tu90fdqgwdt07lg"
sendPresenter.sendToAddress(amount, address)
sendPresenter.sendFunds()
}
//
// Internal View Logic
//
private fun showSendDialog() {
hideKeyboard()
val address = binding.inputZcashAddress.text
val headerString = binding.textValueHeader.text.toString()
val subheaderString = binding.textValueSubheader.text.toString().substringBefore(' ')
val zecString = if(usdSelected) subheaderString else headerString
val usdString = if(usdSelected) headerString else subheaderString
val memo = binding.textAreaMemo.text.toString().trim()
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 = address
binding.dialogTextMemoIncluded.visibility = if(memo.isNotEmpty()) View.VISIBLE else View.GONE
binding.groupDialogSend.visibility = View.VISIBLE
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
}
private fun hideKeyboard() {
@ -352,16 +283,116 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
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
if (isEnabled) {
binding.buttonSendZec.text = "send $zec"
// binding.progressSend.visibility = View.GONE
}
private fun setAddressError(message: String?) {
if (message == null) {
setAddressLineColor()
binding.textAddressError.text = null
binding.textAddressError.visibility = View.GONE
} else {
binding.buttonSendZec.text = "sending..."
// binding.progressSend.visibility = View.VISIBLE
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)
// }
}

View File

@ -1,14 +1,16 @@
package cash.z.android.wallet.ui.presenter
import android.util.Log
import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.entity.Transaction
import cash.z.wallet.sdk.ext.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch
import java.math.BigDecimal
class SendPresenter(
private val view: SendView,
@ -17,13 +19,25 @@ class SendPresenter(
interface SendView : PresenterView {
fun updateBalance(old: Long, new: Long)
fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String)
fun setHeaderValue(usdString: String)
fun setSubheaderValue(usdString: String, isUsdSelected: Boolean)
fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean)
fun validateUserInput(): Boolean
fun submit()
}
private var balanceJob: Job? = null
var sendUiModel = SendUiModel()
//
// LifeCycle
//
override suspend fun start() {
Log.e("@TWIG-v", "sendPresenter starting!")
// set the currency to zec and update the view, intializing everything to zero
toggleCurrency()
with(view) {
balanceJob = launchBalanceBinder(synchronizer.balance())
}
@ -35,27 +49,117 @@ class SendPresenter(
}
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch {
var old: Long? = null
Log.e("@TWIG-v", "send balance binder starting!")
for (new in channel) {
Log.e("@TWIG-v", "send polled a balance item")
bind(old, new).also { old = new }
bind(new)
}
Log.e("@TWIG-v", "send balance binder exiting!")
}
fun sendToAddress(value: Double, toAddress: String) {
//
// Public API
//
fun sendFunds() {
//TODO: prehaps grab the activity scope or let the sycnchronizer have scope and make that function not suspend
// also, we need to handle cancellations. So yeah, definitely do this differently
GlobalScope.launch {
val zatoshi = Math.round(value * 1e8)
synchronizer.sendToAddress(zatoshi, toAddress)
synchronizer.sendToAddress(sendUiModel.zecValue!!, sendUiModel.toAddress)
}
view.submit()
}
fun bind(old: Long?, new: Long) {
Log.e("@TWIG-v", "binding balance of $new")
view.updateBalance(old ?: 0L, new)
/**
* Called when the user has tapped on the button for toggling currency, swapping zec for usd
*/
fun toggleCurrency() {
view.validateUserInput()
sendUiModel = sendUiModel.copy(isUsdSelected = !sendUiModel.isUsdSelected)
with(sendUiModel) {
view.setHeaders(
isUsdSelected = isUsdSelected,
headerString = if (isUsdSelected) usdValue.toUsdString() else zecValue.convertZatoshiToZecString(),
subheaderString = if (isUsdSelected) zecValue.convertZatoshiToZecString() else usdValue.toUsdString()
)
}
}
}
/**
* As the user is typing the header string, update the subheader string. Do not modify our own internal model yet.
* Internal model is only updated after [headerValidated] is called.
*/
fun headerUpdating(headerValue: String) {
headerValue.safelyConvertToBigDecimal()?.let { headerValueAsDecimal ->
val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected)
// subheader string contains opposite currency of the selected one. so if usd is selected, format the subheader as zec
val subheaderString = if(sendUiModel.isUsdSelected) subheaderValue.toZecString() else subheaderValue.toUsdString()
view.setSubheaderValue(subheaderString, sendUiModel.isUsdSelected)
}
}
fun sendPressed() {
with(sendUiModel) {
view.showSendDialog(
zecString = zecValue.convertZatoshiToZecString(),
usdString = usdValue.toUsdString(),
toAddress = toAddress,
hasMemo = !memo.isBlank()
)
}
}
fun headerValidated(amount: BigDecimal) {
with(sendUiModel) {
if (isUsdSelected) {
val headerString = amount.toUsdString()
val usdValue = amount
val zecValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC)
val subheaderString = zecValue.toZecString()
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
} else {
val headerString = amount.toZecString()
val zecValue = amount
val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC)
val subheaderString = usdValue.toUsdString()
sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
println("calling setHeaders with $headerString $subheaderString")
view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
}
}
}
fun addressValidated(address: String) {
sendUiModel = sendUiModel.copy(toAddress = address)
}
/**
* After the user has typed a memo, validated by the UI, then update the model.
*
* assert: this method is only called after the memo input has been validated by the UI
*/
fun memoValidated(sanitizedValue: String) {
sendUiModel = sendUiModel.copy(memo = sanitizedValue)
}
fun bind(newZecBalance: Long) {
if (newZecBalance >= 0) {
Log.e("@TWIG-v", "binding balance of $newZecBalance")
val old = sendUiModel.zecValue
sendUiModel = sendUiModel.copy(zecValue = newZecBalance)
view.updateBalance(old ?: 0L, newZecBalance)
}
}
data class SendUiModel(
val isUsdSelected: Boolean = true,
val zecValue: Long? = null,
val usdValue: BigDecimal = BigDecimal.ZERO,
val toAddress: String = "",
val memo: String = ""
)
}

View File

@ -1,8 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<cash.z.android.cameraview.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
app:autoFocus="true"
app:facing="back"
app:flash="auto">
<View
android:id="@+id/overlay_barcode_scan"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/zcashBlack_54"
android:layout_gravity="center"/>
</cash.z.android.cameraview.CameraView>
</layout>

View File

@ -82,14 +82,20 @@
app:layout_constraintTop_toBottomOf="@id/background_header"
app:layout_constraintVertical_chainStyle="spread_inside" />
<FrameLayout
android:id="@+id/camera_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"/>
<Button
android:id="@+id/button_cancel_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@null"
android:backgroundTint="@null"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/cancel"
android:textColor="@color/zcashRed"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"
app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg" />
<TextView
android:id="@+id/text_amount_background"
@ -108,7 +114,7 @@
android:id="@+id/image_swap_currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="28dp"
android:layout_marginRight="18dp"
android:backgroundTint="@color/zcashPrimaryMedium"
android:foregroundTint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
@ -123,6 +129,7 @@
android:background="@null"
android:inputType="numberDecimal"
android:minWidth="12dp"
android:maxLength="8"
android:text="0"
android:textColor="@color/text_dark"
android:textSize="@dimen/text_size_h3"
@ -160,6 +167,7 @@
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:tint="@color/text_dark"
tools:visibility="invisible"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
app:layout_constraintTop_toTopOf="@id/text_value_subheader"
@ -169,24 +177,26 @@
android:id="@+id/text_dollar_symbol_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="$"
android:textColor="@color/text_dark"
android:textSize="18dp"
android:includeFontPadding="false"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/image_zec_symbol_header"
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_header" />
tools:visibility="invisible"
app:layout_constraintEnd_toStartOf="@id/text_value_header"
app:layout_constraintBottom_toBottomOf="@id/image_zec_symbol_header"
/>
<TextView
android:id="@+id/text_dollar_symbol_subheader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:includeFontPadding="false"
android:layout_marginRight="2dp"
android:text="$"
android:textColor="@color/text_dark"
android:textSize="8dp"
app:layout_constraintEnd_toStartOf="@id/image_zec_symbol_subheader"
app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" />
<!-- Address -->
@ -195,7 +205,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_hint_input_zcash_address"
android:paddingRight="68dp"
android:paddingRight="76dp"
android:singleLine="true"
app:backgroundTint="@color/zcashBlack_12"
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
@ -232,7 +242,7 @@
android:id="@+id/image_address_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginRight="16dp"
app:layout_constraintBottom_toBottomOf="@id/image_scan_qr"
app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
app:layout_constraintTop_toTopOf="@id/image_scan_qr"
@ -307,6 +317,17 @@
app:layout_constraintEnd_toEndOf="@id/divider_memo"
app:layout_constraintTop_toBottomOf="@id/divider_memo" />
<!-- Scan Area -->
<FrameLayout
android:id="@+id/camera_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
tools:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg" />
<!-- -->
<!-- Dialog -->
<!-- -->
@ -320,7 +341,7 @@
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
tools:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dialog_send_contents"
@ -338,7 +359,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.80"
tools:visibility="visible">
tools:visibility="gone">
<TextView
android:id="@+id/dialog_text_title"
@ -409,10 +430,10 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_background"
app:layout_goneMarginTop="32dp"
app:layout_constraintBottom_toTopOf="@+id/dialog_submit_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_text_memo_included" />
app:layout_constraintTop_toBottomOf="@id/dialog_text_memo_included"
app:layout_goneMarginTop="32dp" />
<Button
android:id="@+id/dialog_submit_button"
@ -430,18 +451,6 @@
<!-- Groups -->
<!-- -->
<androidx.constraintlayout.widget.Group
android:id="@+id/group_zec_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="image_zec_symbol_header,text_dollar_symbol_subheader" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_usd_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="image_zec_symbol_subheader,text_dollar_symbol_header" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_dialog_send"
android:layout_width="wrap_content"
@ -449,6 +458,20 @@
android:visibility="gone"
app:constraint_referenced_ids="dialog_send_contents, dialog_send_background" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_hidden_during_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="
transition_active_transaction_bg,
text_value_subheader,
text_dollar_symbol_subheader,
image_zec_symbol_header,
text_dollar_symbol_header,
text_amount_background,
text_value_header,
image_swap_currency" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>

View File

@ -65,9 +65,10 @@
<string name="send_submit_button_text">Send Zec</string>
<string name="send_tooltip_scan_qr">Scan QR Code</string>
<string name="send_tooltip_address_shortcut">Paste Sample Address</string>
<string name="send_subheader_value">0 %1$s</string>
<string name="send_button_label">send %1$s</string>
<string name="send_dialog_title">Send %1$s %2$s ($%3$s)?</string>
<string name="send_alert_shortcut_clicked">Paste a valid sample address for testing?</string>
<string name="send_error_address_too_short">Address is too short.</string>
<string name="send_error_address_invalid_char">Address contains invalid characters.</string>
</resources>

View File

@ -0,0 +1,110 @@
package cash.z.android.wallet.ui.presenter
import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import com.nhaarman.mockitokotlin2.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import java.math.BigDecimal
import java.math.RoundingMode
@ExtendWith(MockitoExtension::class)
internal class SendPresenterTest {
@Mock val view: SendPresenter.SendView = mock()
lateinit var presenter: SendPresenter
@BeforeEach
fun setUp(@Mock synchronizer: Synchronizer) {
presenter = SendPresenter(view, synchronizer)
}
@AfterEach
fun tearDown() {
}
@Test
fun headerUpdating_leadingZeros() {
presenter.headerUpdating("007")
verify(view).setSubheaderValue("0.14", true)
}
@Test
fun headerUpdating_commas() {
presenter.headerUpdating("1,000")
verify(view).setSubheaderValue("20.38", true)
}
@Test
fun headerUpdating_badInputCommas() {
presenter.headerUpdating("34,5")
assertTrue(presenter.sendUiModel.isUsdSelected)
verify(view).setSubheaderValue("7.03", true)
}
@Test
fun headerValidated_roundDown() {
presenter.toggleCurrency()
assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions")
presenter.headerValidated("1.1234561".safelyConvertToBigDecimal()!!)
verify(view, atLeastOnce()).setHeaders(eq(false), eq("1.123456"), eq("55.13"))
}
@Test
fun headerValidated_usdConversion() {
assertTrue(presenter.sendUiModel.isUsdSelected, "expecting USD for this test")
presenter.headerValidated("1000.045".safelyConvertToBigDecimal()!!)
verify(view).setHeaders(eq(true), eq("1,000.04"), eq("20.379967"))
}
@Test
fun headerValidated_roundUp() {
presenter.toggleCurrency()
assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions")
presenter.headerValidated("1.1234556".safelyConvertToBigDecimal()!!)
verify(view).setHeaders(eq(false), eq("1.123456"), eq("55.13"))
}
@Test
fun headerValidated_roundUpBankersRounding() {
// banker's rounding follows odd up, even down
// We'll encourage using that since it has good statistical properties and this rounding only happens in the UI
presenter.toggleCurrency()
assertTrue(!presenter.sendUiModel.isUsdSelected, "zec should be selected to avoid testing conversions")
presenter.headerValidated("1.1234535".safelyConvertToBigDecimal()!!)
assertEquals(112345350, presenter.sendUiModel.zecValue)
assertEquals("1.123454", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "5 is odd, we should round up")
presenter.headerValidated("1.1234565".safelyConvertToBigDecimal()!!)
assertEquals(112345650, presenter.sendUiModel.zecValue)
assertEquals("1.123456", presenter.sendUiModel.zecValue.convertZatoshiToZecString(), "6 is even, we should round down")
}
@Test
fun parseSafely_commas() {
assertEquals("3124", "3,124".safelyConvertToBigDecimal().toString())
}
@Test
fun parseSafely_commasBad() {
assertEquals("3124", ",3124".safelyConvertToBigDecimal().toString())
assertEquals("3124", "3,124".safelyConvertToBigDecimal().toString())
assertEquals("3124", "31,24".safelyConvertToBigDecimal().toString())
assertEquals("3124", "312,4".safelyConvertToBigDecimal().toString())
assertEquals("3124", "3124,".safelyConvertToBigDecimal().toString())
assertEquals("3124", ",3,1,2,4,".safelyConvertToBigDecimal().toString())
}
@Test
fun parseSafely_spaces() {
assertEquals("3124", " 3124".safelyConvertToBigDecimal().toString())
assertEquals("3124", "3 124".safelyConvertToBigDecimal().toString())
assertEquals("3124", "31 24".safelyConvertToBigDecimal().toString())
assertEquals("3124", "312 4".safelyConvertToBigDecimal().toString())
assertEquals("3124", "3124 ".safelyConvertToBigDecimal().toString())
assertEquals("3124", " 3 1 2 4 ".safelyConvertToBigDecimal().toString())
assertEquals("3124", " 3 12 4 ".safelyConvertToBigDecimal().toString())
}
}

View File

@ -16,31 +16,39 @@
package cash.z.android.cameraview
import cash.z.android.qrecycler.R
import android.app.Activity
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.Surface
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.annotation.Nullable
import androidx.core.os.ParcelableCompat
import androidx.core.os.ParcelableCompatCreatorCallbacks
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import cash.z.android.cameraview.api21.Camera2
import cash.z.android.cameraview.base.AspectRatio
import cash.z.android.cameraview.base.CameraViewImpl
import cash.z.android.cameraview.base.Constants
import cash.z.android.cameraview.base.PreviewImpl
import cash.z.android.qrecycler.R
import com.google.android.cameraview.Camera2Api23
import com.google.android.cameraview.TextureViewPreview
import java.lang.IllegalStateException
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
import java.util.*
open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
@ -56,6 +64,16 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
private var mDisplayOrientationDetector: DisplayOrientationDetector?
var firebaseCallback: FirebaseCallback? = null
set(value) {
(mImpl as? Camera2)?.firebaseCallback = value
field = value
}
lateinit var cameraId: String
/**
* @return `true` if the camera is opened.
*/
@ -293,6 +311,18 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
}
}
private var rectPaint = Paint().apply {
color = Color.GREEN
style = Paint.Style.FILL
strokeWidth = 8f
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
val rect = RectF(0f,0f,canvas.width.toFloat(),canvas.height.toFloat())
canvas.drawRect(rect, rectPaint)
}
override fun onSaveInstanceState(): Parcelable? {
val state = SavedState(super.onSaveInstanceState())
state.facing = facing
@ -329,6 +359,11 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
// onRestoreInstanceState(state)
// mImpl.start()
}
// start results in cameraId being set so bubble that up for firebase rotation use
when(mImpl) {
is Camera2 -> cameraId = (mImpl as Camera2).cameraId!!
}
}
/**
@ -367,6 +402,56 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
mImpl.takePicture()
}
fun setBarcode(barcode: FirebaseVisionBarcode) {
}
interface FirebaseCallback {
fun onImageAvailable(image: Image)
// TODO: attribute this code. The library I found it in has no attribution but it clearly came from somewhere. Modified it to not require instantiating a sparsearray of orientations (just use when instead) also simplified method signature
// one source : https://github.com/firebase/snippets-android/blob/master/mlkit/app/src/main/java/com/google/firebase/example/mlkit/VisionImage.java
/**
* Get the angle by which an image must be rotated given the device's current
* orientation.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
fun getRotationCompensation(cameraId: String, activity: Activity): Int {
// Get the device's current rotation relative to its "native" orientation.
// Then, from the ORIENTATIONS table, look up the angle the image must be
// rotated to compensate for the device's rotation.
val deviceRotation = activity.windowManager.defaultDisplay.rotation
var rotationCompensation = when(deviceRotation) {
Surface.ROTATION_0 -> 90
Surface.ROTATION_90 -> 0
Surface.ROTATION_180 -> 270
Surface.ROTATION_270 -> 180
else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation")
}
// On most devices, the sensor orientation is 90 degrees, but for some
// devices it is 270 degrees. For devices with a sensor orientation of
// 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
val cameraManager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
val sensorOrientation = cameraManager
.getCameraCharacteristics(cameraId)
.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
// Return the corresponding FirebaseVisionImageMetadata rotation value.
val result: Int
when (rotationCompensation) {
0 -> result = FirebaseVisionImageMetadata.ROTATION_0
90 -> result = FirebaseVisionImageMetadata.ROTATION_90
180 -> result = FirebaseVisionImageMetadata.ROTATION_180
270 -> result = FirebaseVisionImageMetadata.ROTATION_270
else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation") // this should be impossible, given that we would have already thrown an exception
}
return result
}
}
private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
private val mCallbacks = ArrayList<Callback>()
@ -465,14 +550,14 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
*
* @param cameraView The associated [CameraView].
*/
fun onCameraOpened(cameraView: CameraView) {}
open fun onCameraOpened(cameraView: CameraView) {}
/**
* Called when camera is closed.
*
* @param cameraView The associated [CameraView].
*/
fun onCameraClosed(cameraView: CameraView) {}
open fun onCameraClosed(cameraView: CameraView) {}
/**
* Called when a picture is taken.
@ -480,7 +565,7 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
* @param cameraView The associated [CameraView].
* @param data JPEG data.
*/
fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
open fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
}
companion object {

View File

@ -23,18 +23,24 @@ import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.ImageReader
import android.os.Handler
import android.util.Log
import android.util.SparseIntArray
import androidx.annotation.NonNull
import androidx.annotation.RequiresPermission
import cash.z.android.cameraview.CameraView
import cash.z.android.cameraview.base.*
import java.util.*
import android.os.HandlerThread
@TargetApi(21)
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
private val mCameraManager: CameraManager
var firebaseCallback: CameraView.FirebaseCallback? = null
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
override fun onOpened(@NonNull camera: CameraDevice) {
@ -70,7 +76,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, null
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
@ -101,7 +107,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
)
setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
try {
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, null)
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, backgroundHandler)
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
@ -119,19 +125,21 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
}
private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
reader.acquireNextImage().use { image ->
val planes = image.planes
if (planes.size > 0) {
val buffer = planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)
mCallback.onPictureTaken(data)
if (planes.isNotEmpty()) {
System.err.println("camoorah : planes was empty: $firebaseCallback")
firebaseCallback?.onImageAvailable(image)
try{ image.close() } catch(t: Throwable){ System.err.println("camoorah : failed to close")}
} else {
System.err.println("planes was empty")
}
}
}
private var mCameraId: String? = null
var cameraId: String? = null
private var mCameraCharacteristics: CameraCharacteristics? = null
@ -167,7 +175,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, null
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
field = saved
@ -212,7 +220,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, null
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
mAutoFocus = !mAutoFocus
@ -236,6 +244,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
if (!chooseCameraIdByFacing()) {
return false
}
startBackgroundThread()
collectCameraInfo()
prepareImageReader()
startOpeningCamera()
@ -243,6 +252,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
}
override fun stop() {
stopBackgroundThread()
if (mCaptureSession != null) {
mCaptureSession!!.close()
mCaptureSession = null
@ -314,14 +324,14 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
?: throw NullPointerException("Unexpected state: LENS_FACING null")
if (internal == internalFacing) {
mCameraId = id
cameraId = id
mCameraCharacteristics = characteristics
return true
}
}
// Not found
mCameraId = ids[0]
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId!!)
cameraId = ids[0]
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId!!)
val level = mCameraCharacteristics!!.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
)
@ -359,7 +369,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
private fun collectCameraInfo() {
val map = mCameraCharacteristics!!.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: throw IllegalStateException("Failed to get configuration map: " + mCameraId!!)
) ?: throw IllegalStateException("Failed to get configuration map: " + cameraId!!)
mPreviewSizes.clear()
for (size in map.getOutputSizes(mPreview.outputClass)) {
val width = size.width
@ -392,12 +402,12 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
if (mImageReader != null) {
mImageReader!!.close()
}
val largest = mPictureSizes.sizes(mAspectRatio).last()
// val largest = mPictureSizes.sizes(mAspectRatio).last()
val previewSize = chooseOptimalSize()
mImageReader = ImageReader.newInstance(
largest.width, largest.height,
ImageFormat.JPEG, /* maxImages */ 2
previewSize.width / 4, previewSize.height / 4, ImageFormat.YUV_420_888, 2
)
mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, null)
mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, backgroundHandler)
}
/**
@ -409,9 +419,9 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
@RequiresPermission(Manifest.permission.CAMERA)
private fun startOpeningCamera() {
try {
mCameraManager.openCamera(mCameraId!!, mCameraDeviceCallback, null)
mCameraManager.openCamera(cameraId!!, mCameraDeviceCallback, backgroundHandler)
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to open camera: " + mCameraId!!, e)
throw RuntimeException("Failed to open camera: " + cameraId!!, e)
}
}
@ -434,9 +444,10 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
mPreviewRequestBuilder!!.addTarget(surface)
mPreviewRequestBuilder!!.addTarget(mImageReader!!.surface)
mCamera!!.createCaptureSession(
Arrays.asList(surface, mImageReader!!.surface),
mSessionCallback, null
listOf(surface, mImageReader!!.surface),
mSessionCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to start camera session")
@ -572,7 +583,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
)
try {
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to lock focus.", e)
}
@ -583,6 +594,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
* Captures a still picture.
*/
fun captureStillPicture() {
Log.e("camoorah", "capturing still picture")
try {
val captureRequestBuilder = mCamera!!.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
@ -647,7 +659,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
) {
unlockFocus()
}
}, null
}, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Cannot capture a still picture.", e)
@ -665,21 +677,47 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
try {
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
updateAutoFocus()
updateFlash()
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
)
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to restart camera preview.", e)
}
}
var backgroundHandlerThread: HandlerThread? = null
var backgroundHandler: Handler? = null
/**
* Starts a background thread and its [Handler].
*/
private fun startBackgroundThread() {
backgroundHandlerThread = HandlerThread("CameraBackgroundProcessor")
backgroundHandlerThread?.start()
backgroundHandler = Handler(backgroundHandlerThread?.looper)
}
/**
* Stops the background thread and its [Handler].
*/
private fun stopBackgroundThread() {
backgroundHandlerThread?.quitSafely()
try {
backgroundHandlerThread?.join()
backgroundHandlerThread = null
backgroundHandler = null
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
/**
* A [CameraCaptureSession.CaptureCallback] for capturing a still picture.
*/

View File

@ -7,6 +7,7 @@ import com.google.android.gms.tasks.Task
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetector
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import java.io.IOException
@ -18,8 +19,11 @@ class BarcodeScanningProcessor : VisionProcessorBase<List<FirebaseVisionBarcode>
// FirebaseVisionBarcodeDetectorOptions.Builder()
// .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
// .build()
public val detector: FirebaseVisionBarcodeDetector by lazy {
FirebaseVision.getInstance().visionBarcodeDetector
val detector: FirebaseVisionBarcodeDetector by lazy {
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
.setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
.build()
FirebaseVision.getInstance().getVisionBarcodeDetector(options)
}