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
|
debugImplementation deps.stetho
|
||||||
mockImplementation 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.runner
|
||||||
androidTestImplementation deps.androidx.test.espresso
|
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.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<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" />
|
<dist:module dist:instant="true" />
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package cash.z.android.wallet.sample
|
package cash.z.android.wallet.sample
|
||||||
|
|
||||||
import cash.z.wallet.sdk.data.SampleSeedProvider
|
import cash.z.wallet.sdk.data.SampleSeedProvider
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.MathContext
|
||||||
|
|
||||||
object AliceWallet {
|
object AliceWallet {
|
||||||
const val name = "test.reference.alice"
|
const val name = "test.reference.alice"
|
||||||
|
@ -45,11 +47,5 @@ object SampleProperties {
|
||||||
const val COMPACT_BLOCK_PORT = 9067
|
const val COMPACT_BLOCK_PORT = 9067
|
||||||
val wallet = AliceWallet
|
val wallet = AliceWallet
|
||||||
// TODO: placeholder until we have a network service for this
|
// TODO: placeholder until we have a network service for this
|
||||||
const val USD_PER_ZEC = 49.07
|
val USD_PER_ZEC = BigDecimal("49.07", MathContext.DECIMAL128)
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
}
|
|
@ -20,7 +20,6 @@ import androidx.navigation.ui.setupWithNavController
|
||||||
import cash.z.android.wallet.BuildConfig
|
import cash.z.android.wallet.BuildConfig
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.ZcashWalletApplication
|
import cash.z.android.wallet.ZcashWalletApplication
|
||||||
import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import dagger.android.support.DaggerAppCompatActivity
|
import dagger.android.support.DaggerAppCompatActivity
|
||||||
|
@ -49,12 +48,12 @@ class MainActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
if(!DEV_MODE)synchronizer.start(this)
|
synchronizer.start(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if(!DEV_MODE)synchronizer.stop()
|
synchronizer.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.extention.toAppColor
|
import cash.z.android.wallet.extention.toAppColor
|
||||||
import cash.z.wallet.sdk.dao.WalletTransaction
|
import cash.z.wallet.sdk.dao.WalletTransaction
|
||||||
|
import cash.z.wallet.sdk.ext.convertZatoshiToZec
|
||||||
import cash.z.wallet.sdk.ext.toZec
|
import cash.z.wallet.sdk.ext.toZec
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -40,7 +41,7 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
val sign = if(tx.isSend) "-" else "+"
|
val sign = if(tx.isSend) "-" else "+"
|
||||||
val amountColor = if (tx.isSend) R.color.text_dark_dimmed else R.color.colorPrimary
|
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 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())
|
status.setBackgroundColor(transactionColor.toAppColor())
|
||||||
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending" else formatter.format(tx.timeInSeconds * 1000)
|
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending" else formatter.format(tx.timeInSeconds * 1000)
|
||||||
Log.e("TWIG-z", "TimeInSeconds: ${tx.timeInSeconds}")
|
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.databinding.FragmentHomeBinding
|
||||||
import cash.z.android.wallet.extention.*
|
import cash.z.android.wallet.extention.*
|
||||||
import cash.z.android.wallet.sample.SampleProperties
|
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.adapter.TransactionAdapter
|
||||||
import cash.z.android.wallet.ui.presenter.HomePresenter
|
import cash.z.android.wallet.ui.presenter.HomePresenter
|
||||||
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
|
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.ActiveSendTransaction
|
||||||
import cash.z.wallet.sdk.data.ActiveTransaction
|
import cash.z.wallet.sdk.data.ActiveTransaction
|
||||||
import cash.z.wallet.sdk.data.TransactionState
|
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.google.android.material.snackbar.Snackbar
|
||||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||||
import dagger.Module
|
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
|
//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) {
|
override fun updateBalance(old: Long, new: Long) {
|
||||||
val zecValue = new/1e8
|
val zecValue = new.convertZatoshiToZec()
|
||||||
setZecValue(zecValue)
|
setZecValue(zecValue.toZecString(3))
|
||||||
setUsdValue(SampleProperties.USD_PER_ZEC * zecValue)
|
setUsdValue(zecValue.convertZecToUsd(SampleProperties.USD_PER_ZEC).toUsdString())
|
||||||
|
|
||||||
onContentRefreshComplete(zecValue)
|
onContentRefreshComplete(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTransactions(transactions: List<WalletTransaction>) {
|
override fun setTransactions(transactions: List<WalletTransaction>) {
|
||||||
|
@ -245,12 +244,12 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
|
||||||
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
|
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
|
||||||
setActiveTransactionsShown(true)
|
setActiveTransactionsShown(true)
|
||||||
Log.e("TWIG", "setting transaction state to ${transactionState::class.simpleName}")
|
Log.e("TWIG", "setting transaction state to ${transactionState::class.simpleName}")
|
||||||
var title = "Active Transaction"
|
var title = binding.includeContent.textActiveTransactionTitle.text?.toString() ?: ""
|
||||||
var subtitle = "Processing..."
|
var subtitle = binding.includeContent.textActiveTransactionSubtitle.text?.toString() ?: ""
|
||||||
when (transactionState) {
|
when (transactionState) {
|
||||||
TransactionState.Creating -> {
|
TransactionState.Creating -> {
|
||||||
binding.includeContent.headerActiveTransaction.visibility = View.VISIBLE
|
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}"
|
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}"
|
||||||
setTransactionActive(transaction, true)
|
setTransactionActive(transaction, true)
|
||||||
}
|
}
|
||||||
|
@ -281,7 +280,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
|
||||||
binding.includeContent.lottieActiveTransaction.playAnimation()
|
binding.includeContent.lottieActiveTransaction.playAnimation()
|
||||||
title = "ZEC Sent"
|
title = "ZEC Sent"
|
||||||
subtitle = "Today at 2:11pm"
|
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.textActiveTransactionValue.visibility = View.VISIBLE
|
||||||
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
|
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
@ -405,8 +404,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUsdValue(value: Double) {
|
private fun setUsdValue(valueString: String) {
|
||||||
val valueString = String.format("$ %,.2f",value)
|
|
||||||
val hairSpace = "\u200A"
|
val hairSpace = "\u200A"
|
||||||
// val adjustedValue = "$$hairSpace$valueString"
|
// val adjustedValue = "$$hairSpace$valueString"
|
||||||
val textSpan = SpannableString(valueString)
|
val textSpan = SpannableString(valueString)
|
||||||
|
@ -415,8 +413,8 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
|
||||||
binding.includeHeader.textBalanceUsd.text = textSpan
|
binding.includeHeader.textBalanceUsd.text = textSpan
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setZecValue(value: Double) {
|
private fun setZecValue(value: String) {
|
||||||
binding.includeHeader.textBalanceZec.text = if(value == 0.0) "0" else String.format("%.3f",value)
|
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.
|
// // 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.
|
* 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.
|
* But don't do either of these things if the situation has not changed.
|
||||||
*/
|
*/
|
||||||
private fun onContentRefreshComplete(value: Double) {
|
private fun onContentRefreshComplete(value: Long) {
|
||||||
val isEmpty = value <= 0.0
|
val isEmpty = value <= 0L
|
||||||
// wasEmpty isn't enough info. it must be considered along with whether these views were ever initialized
|
// 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
|
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
|
// 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 headerEmptyViews: Array<View>
|
||||||
lateinit var headerFullViews: 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() {
|
fun forceRedraw() {
|
||||||
view?.postDelayed({
|
view?.postDelayed({
|
||||||
binding.includeHeader.containerHomeHeader.progress = binding.includeHeader.containerHomeHeader.progress - 0.1f
|
binding.includeHeader.containerHomeHeader.progress = binding.includeHeader.containerHomeHeader.progress - 0.1f
|
||||||
}, delay * 2)
|
}, delay * 2)
|
||||||
}
|
}
|
||||||
internal fun toggle(isEmpty: Boolean) {
|
// internal fun toggle(isEmpty: Boolean) {
|
||||||
toggleValues(isEmpty)
|
// toggleValues(isEmpty)
|
||||||
}
|
// }
|
||||||
|
|
||||||
internal fun toggleViews(isEmpty: Boolean) {
|
internal fun toggleViews(isEmpty: Boolean) {
|
||||||
Log.e("TWIG-t", "toggling views to isEmpty == $isEmpty")
|
Log.e("TWIG-t", "toggling views to isEmpty == $isEmpty")
|
||||||
|
@ -643,14 +614,14 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
|
||||||
view?.postDelayed(::forceRedraw, delay * 2)
|
view?.postDelayed(::forceRedraw, delay * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun toggleValues(isEmpty: Boolean) {
|
// internal fun toggleValues(isEmpty: Boolean) {
|
||||||
empty = isEmpty
|
// empty = isEmpty
|
||||||
if(empty) {
|
// if(empty) {
|
||||||
reduceValue()
|
// reduceValue()
|
||||||
} else {
|
// } else {
|
||||||
increaseValue(Random.nextDouble(20.0, 100.0))
|
// increaseValue(Random.nextDouble(20.0, 100.0))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
inner class HomeTransitionListener : Transition.TransitionListener {
|
inner class HomeTransitionListener : Transition.TransitionListener {
|
||||||
|
|
|
@ -1,27 +1,67 @@
|
||||||
package cash.z.android.wallet.ui.fragment
|
package cash.z.android.wallet.ui.fragment
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewAnimationUtils
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import cash.z.android.cameraview.CameraView
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.databinding.FragmentScanBinding
|
import cash.z.android.wallet.databinding.FragmentScanBinding
|
||||||
|
import cash.z.android.wallet.extention.Toaster
|
||||||
import cash.z.android.wallet.ui.activity.MainActivity
|
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.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment for scanning addresss, hopefully.
|
* Fragment for scanning addresss, hopefully.
|
||||||
*/
|
*/
|
||||||
class ScanFragment : BaseFragment() {
|
class ScanFragment : BaseFragment() {
|
||||||
|
|
||||||
lateinit var binding: FragmentScanBinding
|
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?>
|
private val requiredPermissions: Array<String?>
|
||||||
get() {
|
get() {
|
||||||
|
@ -77,6 +117,8 @@ class ScanFragment : BaseFragment() {
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
binding.overlayBarcodeScan.post(revealCamera)
|
||||||
|
System.err.println("camoorah : onResume ScanFragment")
|
||||||
if(allPermissionsGranted()) onStartCamera()
|
if(allPermissionsGranted()) onStartCamera()
|
||||||
// launch {
|
// launch {
|
||||||
// sendPresenter.start()
|
// sendPresenter.start()
|
||||||
|
@ -161,9 +203,62 @@ class ScanFragment : BaseFragment() {
|
||||||
|
|
||||||
private fun onStartCamera() {
|
private fun onStartCamera() {
|
||||||
with(binding.cameraView) {
|
with(binding.cameraView) {
|
||||||
|
// workaround race conditions with google play services downloading the binaries for Firebase Vision APIs
|
||||||
postDelayed({
|
postDelayed({
|
||||||
|
firebaseCallback = PoCallback()
|
||||||
start()
|
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
|
@Module
|
||||||
abstract class ScanFragmentModule {
|
abstract class ScanFragmentModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
|
|
|
@ -17,56 +17,43 @@ import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
import androidx.fragment.app.Fragment
|
||||||
import cash.z.android.qrecycler.QScanner
|
|
||||||
import cash.z.android.wallet.BuildConfig
|
import cash.z.android.wallet.BuildConfig
|
||||||
import cash.z.android.wallet.R
|
import cash.z.android.wallet.R
|
||||||
import cash.z.android.wallet.databinding.FragmentSendBinding
|
import cash.z.android.wallet.databinding.FragmentSendBinding
|
||||||
import cash.z.android.wallet.extention.*
|
import cash.z.android.wallet.extention.*
|
||||||
import cash.z.android.wallet.sample.SampleProperties
|
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.activity.MainActivity
|
||||||
import cash.z.android.wallet.ui.presenter.SendPresenter
|
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.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment for sending Zcash.
|
* 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 sendPresenter: SendPresenter
|
||||||
lateinit var binding: FragmentSendBinding
|
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 zec = R.string.zec_abbreviation.toAppString()
|
||||||
private val usd = R.string.usd_abbreviation.toAppString()
|
private val usd = R.string.usd_abbreviation.toAppString()
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
//
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): 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>(
|
return DataBindingUtil.inflate<FragmentSendBinding>(
|
||||||
inflater, R.layout.fragment_send, container, false
|
inflater, R.layout.fragment_send, container, false
|
||||||
).let {
|
).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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
(activity as MainActivity).let { mainActivity ->
|
(activity as MainActivity).let { mainActivity ->
|
||||||
|
@ -83,119 +75,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
||||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
|
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
|
||||||
}
|
}
|
||||||
init()
|
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?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
@ -208,7 +87,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
||||||
launch {
|
launch {
|
||||||
sendPresenter.start()
|
sendPresenter.start()
|
||||||
}
|
}
|
||||||
if(DEV_MODE) showSendDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -216,56 +94,137 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
||||||
sendPresenter.stop()
|
sendPresenter.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// SendView Implementation
|
||||||
|
//
|
||||||
|
|
||||||
override fun submit() {
|
override fun submit() {
|
||||||
submitNoAnimations()
|
mainActivity.navController.navigate(R.id.nav_home_fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submitNoAnimations() {
|
override fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String) {
|
||||||
mainActivity.navController.navigate(
|
showCurrencySymbols(isUsdSelected)
|
||||||
R.id.nav_home_fragment,
|
setHeaderValue(headerString)
|
||||||
null,
|
setSubheaderValue(subheaderString, isUsdSelected)
|
||||||
null,
|
|
||||||
FragmentNavigatorExtras(binding.dialogTextTitle to "transition_active_transaction_title")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitWithSharedElements() {
|
override fun setHeaderValue(value: String) {
|
||||||
var extras = with(binding) {
|
binding.textValueHeader.setText(value)
|
||||||
listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
|
}
|
||||||
.map{ it to it.transitionName }
|
|
||||||
.let { FragmentNavigatorExtras(*it.toTypedArray()) }
|
@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,
|
binding.textValueHeader.apply {
|
||||||
null,
|
afterTextChanged {
|
||||||
null,
|
sendPresenter.headerUpdating(it)
|
||||||
extras)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
private fun showCurrencySymbols(isUsdSelected: Boolean) {
|
||||||
fun onToggleCurrency() {
|
// 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
|
||||||
view?.findFocus()?.clearFocus()
|
if (isUsdSelected) {
|
||||||
formatUserInput()
|
binding.textDollarSymbolHeader.alpha = 1.0f
|
||||||
val isInitiallyUsd = usdSelected // hold this value because we modify visibility here and that's what the value is based on
|
binding.imageZecSymbolSubheader.alpha = 1.0f
|
||||||
val subHeaderValue = binding.textValueSubheader.text.toString().substringBefore(' ')
|
binding.imageZecSymbolHeader.alpha = 0.0f
|
||||||
val currencyLabelAfterToggle = if (isInitiallyUsd) usd else zec // what is selected is about to move to the subheader where the currency is labelled
|
binding.textDollarSymbolSubheader.alpha = 0.0f
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
binding.groupZecSelected.visibility = View.GONE
|
binding.imageZecSymbolHeader.alpha = 1.0f
|
||||||
binding.groupUsdSelected.visibility = View.VISIBLE
|
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 fragment = ScanFragment()
|
||||||
val ft = childFragmentManager.beginTransaction()
|
val ft = childFragmentManager.beginTransaction()
|
||||||
.add(R.id.camera_placeholder, fragment, "camera_fragment")
|
.add(R.id.camera_placeholder, fragment, "camera_fragment")
|
||||||
|
.addToBackStack("camera_fragment_scanning")
|
||||||
.commit()
|
.commit()
|
||||||
// val intent = Intent(mainActivity, CameraQrScanner::class.java)
|
|
||||||
// mainActivity.startActivity(intent)
|
binding.groupHiddenDuringScan.visibility = View.INVISIBLE
|
||||||
// qrCodeScanner.scanBarcode { barcode: Result<String> ->
|
binding.buttonCancelScan.apply {
|
||||||
// if (barcode.isSuccess) {
|
visibility = View.VISIBLE
|
||||||
// binding.inputZcashAddress.setText(barcode.getOrThrow())
|
animate().alpha(1.0f).apply {
|
||||||
// formatAddressInput()
|
duration = 3000L
|
||||||
// } else {
|
}
|
||||||
// Toaster.short("failed to scan QR code")
|
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.
|
// 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) {
|
private fun onPasteShortcutAddress(view: View) {
|
||||||
view.context.alert(R.string.send_alert_shortcut_clicked) {
|
view.context.alert(R.string.send_alert_shortcut_clicked) {
|
||||||
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
|
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
|
||||||
setAddressError(null)
|
validateAddressInput()
|
||||||
hideKeyboard()
|
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() {
|
private fun onSendZec() {
|
||||||
setSendEnabled(false)
|
setSendEnabled(false)
|
||||||
// val currency = if(zecSelected) "ZEC" else "USD"
|
sendPresenter.sendFunds()
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun exitScanMode() {
|
||||||
//
|
val cameraFragment = childFragmentManager.findFragmentByTag("camera_fragment")
|
||||||
// Internal View Logic
|
if (cameraFragment != null) {
|
||||||
//
|
val ft = childFragmentManager.beginTransaction()
|
||||||
|
.remove(cameraFragment)
|
||||||
private fun showSendDialog() {
|
.commit()
|
||||||
hideKeyboard()
|
}
|
||||||
|
binding.buttonCancelScan.visibility = View.GONE
|
||||||
val address = binding.inputZcashAddress.text
|
binding.groupHiddenDuringScan.visibility = View.VISIBLE
|
||||||
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 hideKeyboard() {
|
private fun hideKeyboard() {
|
||||||
|
@ -352,16 +283,116 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
||||||
binding.groupDialogSend.visibility = View.GONE
|
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) {
|
private fun setSendEnabled(isEnabled: Boolean) {
|
||||||
binding.buttonSendZec.isEnabled = isEnabled
|
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 {
|
} else {
|
||||||
binding.buttonSendZec.text = "sending..."
|
setAddressLineColor(R.color.zcashRed)
|
||||||
// binding.progressSend.visibility = View.VISIBLE
|
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
|
package cash.z.android.wallet.ui.presenter
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import cash.z.android.wallet.sample.SampleProperties
|
||||||
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
|
||||||
import cash.z.wallet.sdk.data.Synchronizer
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
class SendPresenter(
|
class SendPresenter(
|
||||||
private val view: SendView,
|
private val view: SendView,
|
||||||
|
@ -17,13 +19,25 @@ class SendPresenter(
|
||||||
|
|
||||||
interface SendView : PresenterView {
|
interface SendView : PresenterView {
|
||||||
fun updateBalance(old: Long, new: Long)
|
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()
|
fun submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var balanceJob: Job? = null
|
private var balanceJob: Job? = null
|
||||||
|
var sendUiModel = SendUiModel()
|
||||||
|
|
||||||
|
//
|
||||||
|
// LifeCycle
|
||||||
|
//
|
||||||
|
|
||||||
override suspend fun start() {
|
override suspend fun start() {
|
||||||
Log.e("@TWIG-v", "sendPresenter starting!")
|
Log.e("@TWIG-v", "sendPresenter starting!")
|
||||||
|
// set the currency to zec and update the view, intializing everything to zero
|
||||||
|
toggleCurrency()
|
||||||
with(view) {
|
with(view) {
|
||||||
balanceJob = launchBalanceBinder(synchronizer.balance())
|
balanceJob = launchBalanceBinder(synchronizer.balance())
|
||||||
}
|
}
|
||||||
|
@ -35,27 +49,117 @@ class SendPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch {
|
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel<Long>) = launch {
|
||||||
var old: Long? = null
|
|
||||||
Log.e("@TWIG-v", "send balance binder starting!")
|
Log.e("@TWIG-v", "send balance binder starting!")
|
||||||
for (new in channel) {
|
for (new in channel) {
|
||||||
Log.e("@TWIG-v", "send polled a balance item")
|
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!")
|
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
|
//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
|
// also, we need to handle cancellations. So yeah, definitely do this differently
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
val zatoshi = Math.round(value * 1e8)
|
synchronizer.sendToAddress(sendUiModel.zecValue!!, sendUiModel.toAddress)
|
||||||
synchronizer.sendToAddress(zatoshi, toAddress)
|
|
||||||
}
|
}
|
||||||
view.submit()
|
view.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(old: Long?, new: Long) {
|
/**
|
||||||
Log.e("@TWIG-v", "binding balance of $new")
|
* Called when the user has tapped on the button for toggling currency, swapping zec for usd
|
||||||
view.updateBalance(old ?: 0L, new)
|
*/
|
||||||
|
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"?>
|
<?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
|
<cash.z.android.cameraview.CameraView
|
||||||
android:id="@+id/camera_view"
|
android:id="@+id/camera_view"
|
||||||
android:layout_width="match_parent"
|
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>
|
</layout>
|
|
@ -82,14 +82,20 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/background_header"
|
app:layout_constraintTop_toBottomOf="@id/background_header"
|
||||||
app:layout_constraintVertical_chainStyle="spread_inside" />
|
app:layout_constraintVertical_chainStyle="spread_inside" />
|
||||||
|
|
||||||
<FrameLayout
|
<Button
|
||||||
android:id="@+id/camera_placeholder"
|
android:id="@+id/button_cancel_scan"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
|
android:layout_marginTop="4dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
android:background="@null"
|
||||||
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
|
android:backgroundTint="@null"
|
||||||
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"/>
|
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
|
<TextView
|
||||||
android:id="@+id/text_amount_background"
|
android:id="@+id/text_amount_background"
|
||||||
|
@ -108,7 +114,7 @@
|
||||||
android:id="@+id/image_swap_currency"
|
android:id="@+id/image_swap_currency"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginRight="28dp"
|
android:layout_marginRight="18dp"
|
||||||
android:backgroundTint="@color/zcashPrimaryMedium"
|
android:backgroundTint="@color/zcashPrimaryMedium"
|
||||||
android:foregroundTint="@color/colorAccent"
|
android:foregroundTint="@color/colorAccent"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
||||||
|
@ -123,6 +129,7 @@
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:inputType="numberDecimal"
|
android:inputType="numberDecimal"
|
||||||
android:minWidth="12dp"
|
android:minWidth="12dp"
|
||||||
|
android:maxLength="8"
|
||||||
android:text="0"
|
android:text="0"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="@dimen/text_size_h3"
|
android:textSize="@dimen/text_size_h3"
|
||||||
|
@ -160,6 +167,7 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:tint="@color/text_dark"
|
android:tint="@color/text_dark"
|
||||||
|
tools:visibility="invisible"
|
||||||
app:layout_constraintDimensionRatio="H,1:1"
|
app:layout_constraintDimensionRatio="H,1:1"
|
||||||
app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
|
app:layout_constraintEnd_toStartOf="@id/text_value_subheader"
|
||||||
app:layout_constraintTop_toTopOf="@id/text_value_subheader"
|
app:layout_constraintTop_toTopOf="@id/text_value_subheader"
|
||||||
|
@ -169,24 +177,26 @@
|
||||||
android:id="@+id/text_dollar_symbol_header"
|
android:id="@+id/text_dollar_symbol_header"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:text="$"
|
android:text="$"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="18dp"
|
android:textSize="18dp"
|
||||||
|
android:includeFontPadding="false"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintEnd_toStartOf="@id/image_zec_symbol_header"
|
tools:visibility="invisible"
|
||||||
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_header" />
|
app:layout_constraintEnd_toStartOf="@id/text_value_header"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/image_zec_symbol_header"
|
||||||
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_dollar_symbol_subheader"
|
android:id="@+id/text_dollar_symbol_subheader"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:includeFontPadding="false"
|
||||||
android:layout_marginRight="2dp"
|
android:layout_marginRight="2dp"
|
||||||
android:text="$"
|
android:text="$"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="8dp"
|
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" />
|
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" />
|
||||||
|
|
||||||
<!-- Address -->
|
<!-- Address -->
|
||||||
|
@ -195,7 +205,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/send_hint_input_zcash_address"
|
android:hint="@string/send_hint_input_zcash_address"
|
||||||
android:paddingRight="68dp"
|
android:paddingRight="76dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
app:backgroundTint="@color/zcashBlack_12"
|
app:backgroundTint="@color/zcashBlack_12"
|
||||||
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
|
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
|
||||||
|
@ -232,7 +242,7 @@
|
||||||
android:id="@+id/image_address_shortcut"
|
android:id="@+id/image_address_shortcut"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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_constraintBottom_toBottomOf="@id/image_scan_qr"
|
||||||
app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
|
app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
|
||||||
app:layout_constraintTop_toTopOf="@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_constraintEnd_toEndOf="@id/divider_memo"
|
||||||
app:layout_constraintTop_toBottomOf="@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 -->
|
<!-- Dialog -->
|
||||||
<!-- -->
|
<!-- -->
|
||||||
|
@ -320,7 +341,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:visibility="visible" />
|
tools:visibility="gone" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/dialog_send_contents"
|
android:id="@+id/dialog_send_contents"
|
||||||
|
@ -338,7 +359,7 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintWidth_default="percent"
|
app:layout_constraintWidth_default="percent"
|
||||||
app:layout_constraintWidth_percent="0.80"
|
app:layout_constraintWidth_percent="0.80"
|
||||||
tools:visibility="visible">
|
tools:visibility="gone">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/dialog_text_title"
|
android:id="@+id/dialog_text_title"
|
||||||
|
@ -409,10 +430,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:background="@color/divider_background"
|
android:background="@color/divider_background"
|
||||||
app:layout_goneMarginTop="32dp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/dialog_submit_button"
|
app:layout_constraintBottom_toTopOf="@+id/dialog_submit_button"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
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
|
<Button
|
||||||
android:id="@+id/dialog_submit_button"
|
android:id="@+id/dialog_submit_button"
|
||||||
|
@ -430,18 +451,6 @@
|
||||||
<!-- Groups -->
|
<!-- 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
|
<androidx.constraintlayout.widget.Group
|
||||||
android:id="@+id/group_dialog_send"
|
android:id="@+id/group_dialog_send"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -449,6 +458,20 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:constraint_referenced_ids="dialog_send_contents, dialog_send_background" />
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</layout>
|
</layout>
|
|
@ -65,9 +65,10 @@
|
||||||
<string name="send_submit_button_text">Send Zec</string>
|
<string name="send_submit_button_text">Send Zec</string>
|
||||||
<string name="send_tooltip_scan_qr">Scan QR Code</string>
|
<string name="send_tooltip_scan_qr">Scan QR Code</string>
|
||||||
<string name="send_tooltip_address_shortcut">Paste Sample Address</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_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_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_too_short">Address is too short.</string>
|
||||||
|
<string name="send_error_address_invalid_char">Address contains invalid characters.</string>
|
||||||
|
|
||||||
</resources>
|
</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
|
package cash.z.android.cameraview
|
||||||
|
|
||||||
import cash.z.android.qrecycler.R
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
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.Build
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.Surface
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.annotation.IntDef
|
import androidx.annotation.IntDef
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import androidx.annotation.Nullable
|
import androidx.annotation.Nullable
|
||||||
import androidx.core.os.ParcelableCompat
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.os.ParcelableCompatCreatorCallbacks
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import cash.z.android.cameraview.api21.Camera2
|
import cash.z.android.cameraview.api21.Camera2
|
||||||
import cash.z.android.cameraview.base.AspectRatio
|
import cash.z.android.cameraview.base.AspectRatio
|
||||||
import cash.z.android.cameraview.base.CameraViewImpl
|
import cash.z.android.cameraview.base.CameraViewImpl
|
||||||
import cash.z.android.cameraview.base.Constants
|
import cash.z.android.cameraview.base.Constants
|
||||||
import cash.z.android.cameraview.base.PreviewImpl
|
import cash.z.android.cameraview.base.PreviewImpl
|
||||||
|
import cash.z.android.qrecycler.R
|
||||||
import com.google.android.cameraview.Camera2Api23
|
import com.google.android.cameraview.Camera2Api23
|
||||||
import com.google.android.cameraview.TextureViewPreview
|
import com.google.android.cameraview.TextureViewPreview
|
||||||
import java.lang.IllegalStateException
|
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
||||||
import java.lang.annotation.Retention
|
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
|
||||||
import java.lang.annotation.RetentionPolicy
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
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?
|
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.
|
* @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? {
|
override fun onSaveInstanceState(): Parcelable? {
|
||||||
val state = SavedState(super.onSaveInstanceState())
|
val state = SavedState(super.onSaveInstanceState())
|
||||||
state.facing = facing
|
state.facing = facing
|
||||||
|
@ -329,6 +359,11 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
|
||||||
// onRestoreInstanceState(state)
|
// onRestoreInstanceState(state)
|
||||||
// mImpl.start()
|
// 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()
|
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 inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
|
||||||
|
|
||||||
private val mCallbacks = ArrayList<Callback>()
|
private val mCallbacks = ArrayList<Callback>()
|
||||||
|
@ -465,14 +550,14 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
|
||||||
*
|
*
|
||||||
* @param cameraView The associated [CameraView].
|
* @param cameraView The associated [CameraView].
|
||||||
*/
|
*/
|
||||||
fun onCameraOpened(cameraView: CameraView) {}
|
open fun onCameraOpened(cameraView: CameraView) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when camera is closed.
|
* Called when camera is closed.
|
||||||
*
|
*
|
||||||
* @param cameraView The associated [CameraView].
|
* @param cameraView The associated [CameraView].
|
||||||
*/
|
*/
|
||||||
fun onCameraClosed(cameraView: CameraView) {}
|
open fun onCameraClosed(cameraView: CameraView) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a picture is taken.
|
* 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 cameraView The associated [CameraView].
|
||||||
* @param data JPEG data.
|
* @param data JPEG data.
|
||||||
*/
|
*/
|
||||||
fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
|
open fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -23,18 +23,24 @@ import android.graphics.ImageFormat
|
||||||
import android.hardware.camera2.*
|
import android.hardware.camera2.*
|
||||||
import android.hardware.camera2.params.StreamConfigurationMap
|
import android.hardware.camera2.params.StreamConfigurationMap
|
||||||
import android.media.ImageReader
|
import android.media.ImageReader
|
||||||
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
|
import cash.z.android.cameraview.CameraView
|
||||||
import cash.z.android.cameraview.base.*
|
import cash.z.android.cameraview.base.*
|
||||||
import java.util.*
|
import android.os.HandlerThread
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@TargetApi(21)
|
@TargetApi(21)
|
||||||
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
|
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
|
||||||
|
|
||||||
private val mCameraManager: CameraManager
|
private val mCameraManager: CameraManager
|
||||||
|
|
||||||
|
var firebaseCallback: CameraView.FirebaseCallback? = null
|
||||||
|
|
||||||
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
|
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
|
||||||
|
|
||||||
override fun onOpened(@NonNull camera: CameraDevice) {
|
override fun onOpened(@NonNull camera: CameraDevice) {
|
||||||
|
@ -70,7 +76,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
try {
|
try {
|
||||||
mCaptureSession!!.setRepeatingRequest(
|
mCaptureSession!!.setRepeatingRequest(
|
||||||
mPreviewRequestBuilder!!.build(),
|
mPreviewRequestBuilder!!.build(),
|
||||||
mCaptureCallback, null
|
mCaptureCallback, backgroundHandler
|
||||||
)
|
)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
|
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)
|
setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
|
||||||
try {
|
try {
|
||||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, null)
|
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, backgroundHandler)
|
||||||
mPreviewRequestBuilder!!.set(
|
mPreviewRequestBuilder!!.set(
|
||||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
|
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
|
||||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
|
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 ->
|
private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
|
||||||
|
|
||||||
reader.acquireNextImage().use { image ->
|
reader.acquireNextImage().use { image ->
|
||||||
val planes = image.planes
|
val planes = image.planes
|
||||||
if (planes.size > 0) {
|
if (planes.isNotEmpty()) {
|
||||||
val buffer = planes[0].buffer
|
System.err.println("camoorah : planes was empty: $firebaseCallback")
|
||||||
val data = ByteArray(buffer.remaining())
|
firebaseCallback?.onImageAvailable(image)
|
||||||
buffer.get(data)
|
try{ image.close() } catch(t: Throwable){ System.err.println("camoorah : failed to close")}
|
||||||
mCallback.onPictureTaken(data)
|
} else {
|
||||||
|
System.err.println("planes was empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var mCameraId: String? = null
|
var cameraId: String? = null
|
||||||
|
|
||||||
private var mCameraCharacteristics: CameraCharacteristics? = null
|
private var mCameraCharacteristics: CameraCharacteristics? = null
|
||||||
|
|
||||||
|
@ -167,7 +175,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
try {
|
try {
|
||||||
mCaptureSession!!.setRepeatingRequest(
|
mCaptureSession!!.setRepeatingRequest(
|
||||||
mPreviewRequestBuilder!!.build(),
|
mPreviewRequestBuilder!!.build(),
|
||||||
mCaptureCallback, null
|
mCaptureCallback, backgroundHandler
|
||||||
)
|
)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
field = saved
|
field = saved
|
||||||
|
@ -212,7 +220,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
try {
|
try {
|
||||||
mCaptureSession!!.setRepeatingRequest(
|
mCaptureSession!!.setRepeatingRequest(
|
||||||
mPreviewRequestBuilder!!.build(),
|
mPreviewRequestBuilder!!.build(),
|
||||||
mCaptureCallback, null
|
mCaptureCallback, backgroundHandler
|
||||||
)
|
)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
mAutoFocus = !mAutoFocus
|
mAutoFocus = !mAutoFocus
|
||||||
|
@ -236,6 +244,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
if (!chooseCameraIdByFacing()) {
|
if (!chooseCameraIdByFacing()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
startBackgroundThread()
|
||||||
collectCameraInfo()
|
collectCameraInfo()
|
||||||
prepareImageReader()
|
prepareImageReader()
|
||||||
startOpeningCamera()
|
startOpeningCamera()
|
||||||
|
@ -243,6 +252,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
stopBackgroundThread()
|
||||||
if (mCaptureSession != null) {
|
if (mCaptureSession != null) {
|
||||||
mCaptureSession!!.close()
|
mCaptureSession!!.close()
|
||||||
mCaptureSession = null
|
mCaptureSession = null
|
||||||
|
@ -314,14 +324,14 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
|
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||||
?: throw NullPointerException("Unexpected state: LENS_FACING null")
|
?: throw NullPointerException("Unexpected state: LENS_FACING null")
|
||||||
if (internal == internalFacing) {
|
if (internal == internalFacing) {
|
||||||
mCameraId = id
|
cameraId = id
|
||||||
mCameraCharacteristics = characteristics
|
mCameraCharacteristics = characteristics
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not found
|
// Not found
|
||||||
mCameraId = ids[0]
|
cameraId = ids[0]
|
||||||
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId!!)
|
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId!!)
|
||||||
val level = mCameraCharacteristics!!.get(
|
val level = mCameraCharacteristics!!.get(
|
||||||
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
|
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
|
||||||
)
|
)
|
||||||
|
@ -359,7 +369,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
private fun collectCameraInfo() {
|
private fun collectCameraInfo() {
|
||||||
val map = mCameraCharacteristics!!.get(
|
val map = mCameraCharacteristics!!.get(
|
||||||
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
|
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
|
||||||
) ?: throw IllegalStateException("Failed to get configuration map: " + mCameraId!!)
|
) ?: throw IllegalStateException("Failed to get configuration map: " + cameraId!!)
|
||||||
mPreviewSizes.clear()
|
mPreviewSizes.clear()
|
||||||
for (size in map.getOutputSizes(mPreview.outputClass)) {
|
for (size in map.getOutputSizes(mPreview.outputClass)) {
|
||||||
val width = size.width
|
val width = size.width
|
||||||
|
@ -392,12 +402,12 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
if (mImageReader != null) {
|
if (mImageReader != null) {
|
||||||
mImageReader!!.close()
|
mImageReader!!.close()
|
||||||
}
|
}
|
||||||
val largest = mPictureSizes.sizes(mAspectRatio).last()
|
// val largest = mPictureSizes.sizes(mAspectRatio).last()
|
||||||
|
val previewSize = chooseOptimalSize()
|
||||||
mImageReader = ImageReader.newInstance(
|
mImageReader = ImageReader.newInstance(
|
||||||
largest.width, largest.height,
|
previewSize.width / 4, previewSize.height / 4, ImageFormat.YUV_420_888, 2
|
||||||
ImageFormat.JPEG, /* maxImages */ 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)
|
@RequiresPermission(Manifest.permission.CAMERA)
|
||||||
private fun startOpeningCamera() {
|
private fun startOpeningCamera() {
|
||||||
try {
|
try {
|
||||||
mCameraManager.openCamera(mCameraId!!, mCameraDeviceCallback, null)
|
mCameraManager.openCamera(cameraId!!, mCameraDeviceCallback, backgroundHandler)
|
||||||
} catch (e: CameraAccessException) {
|
} 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 {
|
try {
|
||||||
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
||||||
mPreviewRequestBuilder!!.addTarget(surface)
|
mPreviewRequestBuilder!!.addTarget(surface)
|
||||||
|
mPreviewRequestBuilder!!.addTarget(mImageReader!!.surface)
|
||||||
mCamera!!.createCaptureSession(
|
mCamera!!.createCaptureSession(
|
||||||
Arrays.asList(surface, mImageReader!!.surface),
|
listOf(surface, mImageReader!!.surface),
|
||||||
mSessionCallback, null
|
mSessionCallback, backgroundHandler
|
||||||
)
|
)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
throw RuntimeException("Failed to start camera session")
|
throw RuntimeException("Failed to start camera session")
|
||||||
|
@ -572,7 +583,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
|
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
|
||||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
Log.e(TAG, "Failed to lock focus.", e)
|
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.
|
* Captures a still picture.
|
||||||
*/
|
*/
|
||||||
fun captureStillPicture() {
|
fun captureStillPicture() {
|
||||||
|
Log.e("camoorah", "capturing still picture")
|
||||||
try {
|
try {
|
||||||
val captureRequestBuilder = mCamera!!.createCaptureRequest(
|
val captureRequestBuilder = mCamera!!.createCaptureRequest(
|
||||||
CameraDevice.TEMPLATE_STILL_CAPTURE
|
CameraDevice.TEMPLATE_STILL_CAPTURE
|
||||||
|
@ -647,7 +659,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
|
||||||
) {
|
) {
|
||||||
unlockFocus()
|
unlockFocus()
|
||||||
}
|
}
|
||||||
}, null
|
}, backgroundHandler
|
||||||
)
|
)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
Log.e(TAG, "Cannot capture a still picture.", e)
|
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
|
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
|
||||||
updateAutoFocus()
|
updateAutoFocus()
|
||||||
updateFlash()
|
updateFlash()
|
||||||
mPreviewRequestBuilder!!.set(
|
mPreviewRequestBuilder!!.set(
|
||||||
CaptureRequest.CONTROL_AF_TRIGGER,
|
CaptureRequest.CONTROL_AF_TRIGGER,
|
||||||
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
|
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
|
||||||
)
|
)
|
||||||
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
|
||||||
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
|
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
|
||||||
} catch (e: CameraAccessException) {
|
} catch (e: CameraAccessException) {
|
||||||
Log.e(TAG, "Failed to restart camera preview.", e)
|
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.
|
* 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.FirebaseVision
|
||||||
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
||||||
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetector
|
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 com.google.firebase.ml.vision.common.FirebaseVisionImage
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@ -18,8 +19,11 @@ class BarcodeScanningProcessor : VisionProcessorBase<List<FirebaseVisionBarcode>
|
||||||
// FirebaseVisionBarcodeDetectorOptions.Builder()
|
// FirebaseVisionBarcodeDetectorOptions.Builder()
|
||||||
// .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
|
// .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
|
||||||
// .build()
|
// .build()
|
||||||
public val detector: FirebaseVisionBarcodeDetector by lazy {
|
val detector: FirebaseVisionBarcodeDetector by lazy {
|
||||||
FirebaseVision.getInstance().visionBarcodeDetector
|
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
|
||||||
|
.setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
|
||||||
|
.build()
|
||||||
|
FirebaseVision.getInstance().getVisionBarcodeDetector(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue