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:
parent
e26d66cc3b
commit
92b9558446
|
@ -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
|
||||
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
)
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue