diff --git a/zcash-android-wallet-app/app/build.gradle b/zcash-android-wallet-app/app/build.gradle
index 3a2d97e..d47fd4e 100644
--- a/zcash-android-wallet-app/app/build.gradle
+++ b/zcash-android-wallet-app/app/build.gradle
@@ -106,7 +106,13 @@ dependencies {
debugImplementation deps.stetho
mockImplementation deps.stetho
- testImplementation deps.junit
+ testImplementation 'org.mockito:mockito-junit-jupiter:2.24.0'
+ testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
+ testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.0"
+ testImplementation "org.junit.jupiter:junit-jupiter-engine:5.4.0"
+ testImplementation "org.junit.jupiter:junit-jupiter-migrationsupport:5.4.0"
+
androidTestImplementation deps.androidx.test.runner
androidTestImplementation deps.androidx.test.espresso
+
}
diff --git a/zcash-android-wallet-app/app/libs/zcash-android-wallet-sdk-1.6.0.aar b/zcash-android-wallet-app/app/libs/zcash-android-wallet-sdk-1.6.0.aar
index c36c550..85887b2 100644
Binary files a/zcash-android-wallet-app/app/libs/zcash-android-wallet-sdk-1.6.0.aar and b/zcash-android-wallet-app/app/libs/zcash-android-wallet-sdk-1.6.0.aar differ
diff --git a/zcash-android-wallet-app/app/src/main/AndroidManifest.xml b/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
index ce71389..90591e0 100644
--- a/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
+++ b/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleConfig.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleConfig.kt
index 2beefd7..ca7e6e7 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleConfig.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleConfig.kt
@@ -1,6 +1,8 @@
package cash.z.android.wallet.sample
import cash.z.wallet.sdk.data.SampleSeedProvider
+import java.math.BigDecimal
+import java.math.MathContext
object AliceWallet {
const val name = "test.reference.alice"
@@ -45,11 +47,5 @@ object SampleProperties {
const val COMPACT_BLOCK_PORT = 9067
val wallet = AliceWallet
// TODO: placeholder until we have a network service for this
- const val USD_PER_ZEC = 49.07
-
- /**
- * A simple flag that helps with removing shortcuts in the code used during development.
- * TODO: either elevate this to a real thing (based off a system property or some such) or delete it!
- */
- const val DEV_MODE = false
+ val USD_PER_ZEC = BigDecimal("49.07", MathContext.DECIMAL128)
}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/activity/MainActivity.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/activity/MainActivity.kt
index f25c827..32873d9 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/activity/MainActivity.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/activity/MainActivity.kt
@@ -20,7 +20,6 @@ import androidx.navigation.ui.setupWithNavController
import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.R
import cash.z.android.wallet.ZcashWalletApplication
-import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.android.support.DaggerAppCompatActivity
@@ -49,12 +48,12 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- if(!DEV_MODE)synchronizer.start(this)
+ synchronizer.start(this)
}
override fun onDestroy() {
super.onDestroy()
- if(!DEV_MODE)synchronizer.stop()
+ synchronizer.stop()
}
override fun onBackPressed() {
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/adapter/TransactionAdapter.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/adapter/TransactionAdapter.kt
index c45c9f5..1df114d 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/adapter/TransactionAdapter.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/adapter/TransactionAdapter.kt
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.android.wallet.R
import cash.z.android.wallet.extention.toAppColor
import cash.z.wallet.sdk.dao.WalletTransaction
+import cash.z.wallet.sdk.ext.convertZatoshiToZec
import cash.z.wallet.sdk.ext.toZec
import java.text.SimpleDateFormat
import java.util.*
@@ -40,7 +41,7 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
val sign = if(tx.isSend) "-" else "+"
val amountColor = if (tx.isSend) R.color.text_dark_dimmed else R.color.colorPrimary
val transactionColor = if(tx.isSend) R.color.send_associated else R.color.receive_associated
- val zecAbsoluteValue = tx.value.absoluteValue.toZec(3)
+ val zecAbsoluteValue = tx.value.absoluteValue.convertZatoshiToZec(3)
status.setBackgroundColor(transactionColor.toAppColor())
timestamp.text = if (!tx.isMined || tx.timeInSeconds == 0L) "Pending" else formatter.format(tx.timeInSeconds * 1000)
Log.e("TWIG-z", "TimeInSeconds: ${tx.timeInSeconds}")
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/HomeFragment.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/HomeFragment.kt
index 9e6b7ff..12ac3cc 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/HomeFragment.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/HomeFragment.kt
@@ -23,7 +23,6 @@ import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentHomeBinding
import cash.z.android.wallet.extention.*
import cash.z.android.wallet.sample.SampleProperties
-import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
import cash.z.android.wallet.ui.adapter.TransactionAdapter
import cash.z.android.wallet.ui.presenter.HomePresenter
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
@@ -33,7 +32,7 @@ import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.data.ActiveSendTransaction
import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.wallet.sdk.data.TransactionState
-import cash.z.wallet.sdk.ext.toZec
+import cash.z.wallet.sdk.ext.*
import com.google.android.material.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem
import dagger.Module
@@ -168,11 +167,11 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
//TODO: pull some of this logic into the presenter, particularly the part that deals with ZEC <-> USD price conversion
override fun updateBalance(old: Long, new: Long) {
- val zecValue = new/1e8
- setZecValue(zecValue)
- setUsdValue(SampleProperties.USD_PER_ZEC * zecValue)
+ val zecValue = new.convertZatoshiToZec()
+ setZecValue(zecValue.toZecString(3))
+ setUsdValue(zecValue.convertZecToUsd(SampleProperties.USD_PER_ZEC).toUsdString())
- onContentRefreshComplete(zecValue)
+ onContentRefreshComplete(new)
}
override fun setTransactions(transactions: List) {
@@ -245,12 +244,12 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
private fun updatePrimaryTransaction(transaction: ActiveTransaction, transactionState: TransactionState) {
setActiveTransactionsShown(true)
Log.e("TWIG", "setting transaction state to ${transactionState::class.simpleName}")
- var title = "Active Transaction"
- var subtitle = "Processing..."
+ var title = binding.includeContent.textActiveTransactionTitle.text?.toString() ?: ""
+ var subtitle = binding.includeContent.textActiveTransactionSubtitle.text?.toString() ?: ""
when (transactionState) {
TransactionState.Creating -> {
binding.includeContent.headerActiveTransaction.visibility = View.VISIBLE
- title = "Preparing ${transaction.value.toZec(3)} ZEC"
+ title = "Preparing ${transaction.value.convertZatoshiToZecString(3)} ZEC"
subtitle = "to ${(transaction as ActiveSendTransaction).toAddress}"
setTransactionActive(transaction, true)
}
@@ -281,7 +280,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
binding.includeContent.lottieActiveTransaction.playAnimation()
title = "ZEC Sent"
subtitle = "Today at 2:11pm"
- binding.includeContent.textActiveTransactionValue.text = transaction.value.toZec(3).toString()
+ binding.includeContent.textActiveTransactionValue.text = transaction.value.convertZatoshiToZecString(3)
binding.includeContent.textActiveTransactionValue.visibility = View.VISIBLE
binding.includeContent.buttonActiveTransactionCancel.visibility = View.GONE
} else {
@@ -405,8 +404,7 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
.create()
}
- private fun setUsdValue(value: Double) {
- val valueString = String.format("$ %,.2f",value)
+ private fun setUsdValue(valueString: String) {
val hairSpace = "\u200A"
// val adjustedValue = "$$hairSpace$valueString"
val textSpan = SpannableString(valueString)
@@ -415,8 +413,8 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
binding.includeHeader.textBalanceUsd.text = textSpan
}
- private fun setZecValue(value: Double) {
- binding.includeHeader.textBalanceZec.text = if(value == 0.0) "0" else String.format("%.3f",value)
+ private fun setZecValue(value: String) {
+ binding.includeHeader.textBalanceZec.text = value
// // bugfix: there is a bug in motionlayout that causes text to flicker as it is resized because the last character doesn't fit. Padding both sides with a thin space works around this bug.
@@ -431,8 +429,8 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
* If the balance changes from zero, the wallet is no longer empty so hide the empty view.
* But don't do either of these things if the situation has not changed.
*/
- private fun onContentRefreshComplete(value: Double) {
- val isEmpty = value <= 0.0
+ private fun onContentRefreshComplete(value: Long) {
+ val isEmpty = value <= 0L
// wasEmpty isn't enough info. it must be considered along with whether these views were ever initialized
val wasEmpty = binding.includeContent.groupEmptyViewItems.visibility == View.VISIBLE
// situation has changed when we weren't initialized but now we have a balance or emptiness has changed
@@ -537,42 +535,15 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
lateinit var headerEmptyViews: Array
lateinit var headerFullViews: Array
- fun shrink(): Double {
- return binding.includeHeader.textBalanceZec.text.toString().trim().toDouble() - Random.nextDouble(5.0)
- }
- fun grow(): Double {
- return binding.includeHeader.textBalanceZec.text.toString().trim().toDouble() + Random.nextDouble(5.0)
- }
- fun reduceValue() {
- shrink().let {
- if(it < 0) { setZecValue(0.0); toggleViews(empty); forceRedraw() }
- else view?.postDelayed({
- setZecValue(it)
- setUsdValue(it*75.0)
- reduceValue()
- }, delay)
- }
- }
- fun increaseValue(target: Double) {
- grow().let {
- if(it > target) { setZecValue(target); setUsdValue(target*75.0); toggleViews(empty) }
- else view?.postDelayed({
- setZecValue(it)
- setUsdValue(it*75.0)
- increaseValue(target)
- if (headerFullViews[0].parent == null || headerEmptyViews[0].parent != null) toggleViews(false)
- forceRedraw()
- }, delay)
- }
- }
+
fun forceRedraw() {
view?.postDelayed({
binding.includeHeader.containerHomeHeader.progress = binding.includeHeader.containerHomeHeader.progress - 0.1f
}, delay * 2)
}
- internal fun toggle(isEmpty: Boolean) {
- toggleValues(isEmpty)
- }
+// internal fun toggle(isEmpty: Boolean) {
+// toggleValues(isEmpty)
+// }
internal fun toggleViews(isEmpty: Boolean) {
Log.e("TWIG-t", "toggling views to isEmpty == $isEmpty")
@@ -643,14 +614,14 @@ class HomeFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, HomeP
view?.postDelayed(::forceRedraw, delay * 2)
}
- internal fun toggleValues(isEmpty: Boolean) {
- empty = isEmpty
- if(empty) {
- reduceValue()
- } else {
- increaseValue(Random.nextDouble(20.0, 100.0))
- }
- }
+// internal fun toggleValues(isEmpty: Boolean) {
+// empty = isEmpty
+// if(empty) {
+// reduceValue()
+// } else {
+// increaseValue(Random.nextDouble(20.0, 100.0))
+// }
+// }
inner class HomeTransitionListener : Transition.TransitionListener {
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ScanFragment.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ScanFragment.kt
index 736c3df..d4b38d8 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ScanFragment.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ScanFragment.kt
@@ -1,27 +1,67 @@
package cash.z.android.wallet.ui.fragment
+import android.animation.Animator
import android.content.Context
import android.content.pm.PackageManager
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.media.Image
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
+import android.view.ViewAnimationUtils
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
+import cash.z.android.cameraview.CameraView
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentScanBinding
+import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.ui.activity.MainActivity
+import com.google.firebase.ml.vision.FirebaseVision
+import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
+import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
+import com.google.firebase.ml.vision.common.FirebaseVisionImage
import dagger.Module
import dagger.android.ContributesAndroidInjector
+
/**
* Fragment for scanning addresss, hopefully.
*/
class ScanFragment : BaseFragment() {
lateinit var binding: FragmentScanBinding
+ var barcodeCallback: BarcodeCallback? = null
-// private var cameraSource: CameraSource? = null
+ interface BarcodeCallback {
+ fun onBarcodeScanned(value: String)
+ }
+
+ private val revealCamera = Runnable {
+ binding.overlayBarcodeScan.apply {
+ val cX = measuredWidth / 2
+ val cY = measuredHeight / 2
+ ViewAnimationUtils.createCircularReveal(this, cX, cY, 0.0f, cX.toFloat()).start()
+ postDelayed({
+ val v:View = this
+ v.animate().alpha(0.0f).apply { duration = 2400L }.setListener(object : Animator.AnimatorListener {
+ override fun onAnimationRepeat(animation: Animator?) {
+ }
+
+ override fun onAnimationStart(animation: Animator?) {
+ }
+
+ override fun onAnimationEnd(animation: Animator?) {
+ binding.overlayBarcodeScan.visibility = View.GONE
+ }
+ override fun onAnimationCancel(animation: Animator?) {
+ binding.overlayBarcodeScan.visibility = View.GONE
+ }
+ })
+ },500L)
+ }
+ }
private val requiredPermissions: Array
get() {
@@ -77,6 +117,8 @@ class ScanFragment : BaseFragment() {
override fun onResume() {
super.onResume()
+ binding.overlayBarcodeScan.post(revealCamera)
+ System.err.println("camoorah : onResume ScanFragment")
if(allPermissionsGranted()) onStartCamera()
// launch {
// sendPresenter.start()
@@ -161,9 +203,62 @@ class ScanFragment : BaseFragment() {
private fun onStartCamera() {
with(binding.cameraView) {
+ // workaround race conditions with google play services downloading the binaries for Firebase Vision APIs
postDelayed({
+ firebaseCallback = PoCallback()
start()
- }, 1500L)
+ }, 1000L)
+ }
+ }
+
+ inner class PoCallback : CameraView.FirebaseCallback {
+ val options = FirebaseVisionBarcodeDetectorOptions.Builder()
+ .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
+ .build()
+ val barcodeDetector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)
+ var cameraId = getBackCameraId()
+
+ private fun getBackCameraId(): String {
+ val manager = mainActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+
+ for (cameraId in manager.cameraIdList) {
+ val characteristics = manager.getCameraCharacteristics(cameraId)
+ val cOrientation = characteristics.get(CameraCharacteristics.LENS_FACING)!!
+ if (cOrientation == CameraCharacteristics.LENS_FACING_BACK) return cameraId
+ }
+ throw IllegalArgumentException("no rear-facing camera found!")
+ }
+
+ override fun onImageAvailable(image: Image) {
+ System.err.println("camoorah : onImageAvailable: $image width: ${image.width} height: ${image.height}")
+ var firebaseImage = FirebaseVisionImage.fromMediaImage(image, getRotationCompensation(cameraId, mainActivity))
+ barcodeDetector
+ .detectInImage(firebaseImage)
+ .addOnSuccessListener { results ->
+ if (results.isNotEmpty()) {
+ val barcode = results[0]
+ val value = barcode.rawValue
+ val message = "found: $value"
+ Toaster.short(message)
+ onScanSuccess(value!!)
+ // TODO: highlight the barcode
+ var bounds = barcode.boundingBox
+ var corners = barcode.cornerPoints
+ binding.cameraView.setBarcode(barcode)
+ }
+ }
+ }
+ }
+
+ private var pendingSuccess = false
+ private fun onScanSuccess(value: String) {
+ if (!pendingSuccess) {
+ pendingSuccess = true
+ with(binding.cameraView) {
+ postDelayed({
+ barcodeCallback?.onBarcodeScanned(value)
+ }, 3000L)
+ }
}
}
@@ -177,7 +272,6 @@ class ScanFragment : BaseFragment() {
}
}
-
@Module
abstract class ScanFragmentModule {
@ContributesAndroidInjector
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/SendFragment.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/SendFragment.kt
index b9585b0..9bbce1e 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/SendFragment.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/SendFragment.kt
@@ -17,56 +17,43 @@ import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.text.toSpannable
import androidx.databinding.DataBindingUtil
-import androidx.navigation.fragment.FragmentNavigatorExtras
-import cash.z.android.qrecycler.QScanner
+import androidx.fragment.app.Fragment
import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentSendBinding
import cash.z.android.wallet.extention.*
import cash.z.android.wallet.sample.SampleProperties
-import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.presenter.SendPresenter
+import cash.z.wallet.sdk.ext.convertZatoshiToZecString
+import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
import java.text.DecimalFormat
-import javax.inject.Inject
import kotlin.math.absoluteValue
-
/**
* Fragment for sending Zcash.
*
*/
-class SendFragment : BaseFragment(), SendPresenter.SendView {
+class SendFragment : BaseFragment(), SendPresenter.SendView, ScanFragment.BarcodeCallback {
- @Inject
- lateinit var qrCodeScanner: QScanner
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
- private val zecFormatter = DecimalFormat("#.######")
- private val usdFormatter = DecimalFormat("###,###,##0.00")
- private val usdSelected get() = binding.groupUsdSelected.visibility == View.VISIBLE
-
private val zec = R.string.zec_abbreviation.toAppString()
private val usd = R.string.usd_abbreviation.toAppString()
+
+ //
+ // Lifecycle
+ //
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
-// val enterTransitionSet = TransitionInflater.from(mainActivity).inflateTransition(R.transition.transition_zec_sent).apply {
-// duration = 3500L
-// }
-//
-// this.sharedElementReturnTransition = enterTransitionSet
-// this.sharedElementEnterTransition = enterTransitionSet
-//
-// this.allowReturnTransitionOverlap = false
-// allowEnterTransitionOverlap = false
-
return DataBindingUtil.inflate(
inflater, R.layout.fragment_send, container, false
).let {
@@ -75,6 +62,11 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
}
}
+ override fun onAttachFragment(childFragment: Fragment?) {
+ super.onAttachFragment(childFragment)
+ (childFragment as? ScanFragment)?.barcodeCallback = this
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).let { mainActivity ->
@@ -83,119 +75,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
}
init()
- initDialog()
- }
-
- // temporary function until presenter is setup
- private fun init() {
- binding.imageSwapCurrency.setOnClickListener {
- onToggleCurrency()
- }
-
- binding.textValueHeader.apply {
- afterTextChanged {
- tryIgnore {
- // only update things if the user is actively editing. in other words, don't update on programmatic changes
- if (binding.textValueHeader.hasFocus()) {
- val value = binding.textValueHeader.text.toString().toDouble()
- binding.textValueSubheader.text = if (usdSelected) {
- zecFormatter.format(value / SampleProperties.USD_PER_ZEC) + " $zec"
- } else {
- if (value == 0.0) "0 $usd"
- else usdFormatter.format(value * SampleProperties.USD_PER_ZEC) + " $usd"
- }
- }
- }
- }
- }
-
- binding.textAreaMemo.afterTextChanged {
- binding.textMemoCharCount.text =
- "${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
- }
-
- binding.buttonSendZec.setOnClickListener {
- showSendDialog()
- }
- binding.buttonSendZec.isEnabled = false
-
- with(binding.imageScanQr) {
- TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
- }
- binding.imageAddressShortcut?.apply {
- if (BuildConfig.DEBUG) {
- TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
- setOnClickListener(::onPasteShortcutAddress)
- } else {
- visibility = View.GONE
- }
- }
- binding.imageScanQr.setOnClickListener(::onScanQrCode)
- binding.textValueHeader.setText("0")
- binding.textValueSubheader.text =
- mainActivity.resources.getString(R.string.send_subheader_value, if (usdSelected) zec else usd)
-
- // allow background taps to dismiss the keyboard and clear focus
- binding.contentFragmentSend.setOnClickListener {
- it?.findFocus()?.clearFocus()
- formatUserInput()
- hideKeyboard()
- }
-
- setSendEnabled(true)
- onToggleCurrency()
- }
-
- private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
- DrawableCompat.setTint(
- binding.inputZcashAddress.background,
- ContextCompat.getColor(mainActivity, colorRes)
- )
- }
-
- fun formatUserInput() {
- formatAmountInput()
- formatAddressInput()
- }
-
- private fun formatAmountInput() {
- val value = binding.textValueHeader.text.toString().toDouble().absoluteValue
- binding.textValueHeader.setText(
- when {
- value == 0.0 -> "0"
- usdSelected -> usdFormatter.format(value)
- else -> zecFormatter.format(value)
- }
- )
- }
-
- private fun formatAddressInput() {
- val address = binding.inputZcashAddress.text
- if(address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
- else setAddressError(null)
- }
-
- private fun setAddressError(message: String?) {
- if (message == null) {
- setAddressLineColor()
- binding.textAddressError.text = null
- binding.textAddressError.visibility = View.GONE
- binding.buttonSendZec.isEnabled = true
- } else {
- setAddressLineColor(R.color.zcashRed)
- binding.textAddressError.text = message
- binding.textAddressError.visibility = View.VISIBLE
- binding.buttonSendZec.isEnabled = false
- }
- }
-
- private fun initDialog() {
- binding.dialogSendBackground.setOnClickListener {
- hideSendDialog()
- }
- binding.dialogSubmitButton.setOnClickListener {
- if (DEV_MODE) submit() else onSendZec()
- }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -208,7 +87,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
launch {
sendPresenter.start()
}
- if(DEV_MODE) showSendDialog()
}
override fun onPause() {
@@ -216,56 +94,137 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
sendPresenter.stop()
}
+
+ //
+ // SendView Implementation
+ //
+
override fun submit() {
- submitNoAnimations()
+ mainActivity.navController.navigate(R.id.nav_home_fragment)
}
- private fun submitNoAnimations() {
- mainActivity.navController.navigate(
- R.id.nav_home_fragment,
- null,
- null,
- FragmentNavigatorExtras(binding.dialogTextTitle to "transition_active_transaction_title")
- )
+ override fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String) {
+ showCurrencySymbols(isUsdSelected)
+ setHeaderValue(headerString)
+ setSubheaderValue(subheaderString, isUsdSelected)
}
- fun submitWithSharedElements() {
- var extras = with(binding) {
- listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
- .map{ it to it.transitionName }
- .let { FragmentNavigatorExtras(*it.toTypedArray()) }
+ override fun setHeaderValue(value: String) {
+ binding.textValueHeader.setText(value)
+ }
+
+ @SuppressLint("SetTextI18n") // SetTextI18n lint logic has errors and does not recognize that the entire string contains variables, formatted per locale and loaded from string resources.
+ override fun setSubheaderValue(value: String, isUsdSelected: Boolean) {
+ val subheaderLabel = if (isUsdSelected) zec else usd
+ binding.textValueSubheader.text = "$value $subheaderLabel" //ignore SetTextI18n error here because it is invalid
+ }
+
+ override fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean) {
+ hideKeyboard()
+ setSendEnabled(false) // partially because we need to lower the button elevation
+ binding.dialogTextTitle.text = getString(R.string.send_dialog_title, zecString, zec, usdString)
+ binding.dialogTextAddress.text = toAddress
+ binding.dialogTextMemoIncluded.visibility = if(hasMemo) View.VISIBLE else View.GONE
+ binding.groupDialogSend.visibility = View.VISIBLE
+ }
+
+ override fun updateBalance(old: Long, new: Long) {
+ // TODO: use a formatted string resource here
+ val availableTextSpan = "${new.convertZatoshiToZecString(8)} $zec Available".toSpannable()
+ availableTextSpan.setSpan(ForegroundColorSpan(R.color.colorPrimary.toAppColor()), availableTextSpan.length - "Available".length, availableTextSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ availableTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ binding.textZecValueAvailable.text = availableTextSpan
+ }
+
+
+ //
+ // ScanFragment.BarcodeCallback implemenation
+ //
+
+ override fun onBarcodeScanned(value: String) {
+ exitScanMode()
+ binding.inputZcashAddress.setText(value)
+ validateAddressInput()
+ }
+
+
+ //
+ // Internal View Logic
+ //
+
+ /**
+ * Initialize view logic only. Click listeners, text change handlers and tooltips.
+ */
+ private fun init() {
+ /* Presenter calls */
+
+ binding.imageSwapCurrency.setOnClickListener {
+ sendPresenter.toggleCurrency()
}
-// val extras = FragmentNavigatorExtras(
-// binding.dialogSendContents to binding.dialogSendContents.transitionName,
-// binding.dialogTextTitle to getString(R.string.transition_active_transaction_title),
-// binding.dialogTextAddress to getString(R.string.transition_active_transaction_address),
-// binding.dialogSendBackground to getString(R.string.transition_active_transaction_background)
-// )
- mainActivity.navController.navigate(R.id.nav_home_fragment,
- null,
- null,
- extras)
+ binding.textValueHeader.apply {
+ afterTextChanged {
+ sendPresenter.headerUpdating(it)
+ }
+ }
+
+ binding.buttonSendZec.setOnClickListener {
+ sendPresenter.sendPressed()
+ }
+
+ /* Non-Presenter calls (UI-only logic) */
+
+ binding.textAreaMemo.afterTextChanged {
+ binding.textMemoCharCount.text =
+ "${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
+ }
+
+ binding.imageScanQr.apply {
+ TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
+ }
+
+ binding.imageAddressShortcut?.apply {
+ if (BuildConfig.DEBUG) {
+ visibility = View.VISIBLE
+ TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
+ setOnClickListener(::onPasteShortcutAddress)
+ } else {
+ visibility = View.GONE
+ }
+ }
+
+ binding.dialogSendBackground.setOnClickListener {
+ hideSendDialog()
+ }
+ binding.dialogSubmitButton.setOnClickListener {
+ onSendZec()
+ }
+
+ binding.imageScanQr.setOnClickListener(::onScanQrCode)
+
+ // allow background taps to dismiss the keyboard and clear focus
+ binding.contentFragmentSend.setOnClickListener {
+ it?.findFocus()?.clearFocus()
+ validateUserInput()
+ hideKeyboard()
+ }
+
+ binding.buttonSendZec.text = getString(R.string.send_button_label, zec)
+ setSendEnabled(false)
}
- @SuppressLint("SetTextI18n")
- fun onToggleCurrency() {
- view?.findFocus()?.clearFocus()
- formatUserInput()
- val isInitiallyUsd = usdSelected // hold this value because we modify visibility here and that's what the value is based on
- val subHeaderValue = binding.textValueSubheader.text.toString().substringBefore(' ')
- val currencyLabelAfterToggle = if (isInitiallyUsd) usd else zec // what is selected is about to move to the subheader where the currency is labelled
-
- binding.textValueSubheader.post {
- binding.textValueSubheader.text = "${binding.textValueHeader.text} $currencyLabelAfterToggle"
- binding.textValueHeader.setText(subHeaderValue)
- }
- if (isInitiallyUsd) {
- binding.groupZecSelected.visibility = View.VISIBLE
- binding.groupUsdSelected.visibility = View.GONE
+ private fun showCurrencySymbols(isUsdSelected: Boolean) {
+ // visibility has some kind of bug that appears to be related to layout groups. So using alpha instead since our API level is high enough to support that
+ if (isUsdSelected) {
+ binding.textDollarSymbolHeader.alpha = 1.0f
+ binding.imageZecSymbolSubheader.alpha = 1.0f
+ binding.imageZecSymbolHeader.alpha = 0.0f
+ binding.textDollarSymbolSubheader.alpha = 0.0f
} else {
- binding.groupZecSelected.visibility = View.GONE
- binding.groupUsdSelected.visibility = View.VISIBLE
+ binding.imageZecSymbolHeader.alpha = 1.0f
+ binding.textDollarSymbolSubheader.alpha = 1.0f
+ binding.textDollarSymbolHeader.alpha = 0.0f
+ binding.imageZecSymbolSubheader.alpha = 0.0f
}
}
@@ -274,72 +233,44 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
val fragment = ScanFragment()
val ft = childFragmentManager.beginTransaction()
.add(R.id.camera_placeholder, fragment, "camera_fragment")
+ .addToBackStack("camera_fragment_scanning")
.commit()
-// val intent = Intent(mainActivity, CameraQrScanner::class.java)
-// mainActivity.startActivity(intent)
-// qrCodeScanner.scanBarcode { barcode: Result ->
-// if (barcode.isSuccess) {
-// binding.inputZcashAddress.setText(barcode.getOrThrow())
-// formatAddressInput()
-// } else {
-// Toaster.short("failed to scan QR code")
-// }
-// }
+
+ binding.groupHiddenDuringScan.visibility = View.INVISIBLE
+ binding.buttonCancelScan.apply {
+ visibility = View.VISIBLE
+ animate().alpha(1.0f).apply {
+ duration = 3000L
+ }
+ setOnClickListener {
+ exitScanMode()
+ }
+ }
}
// TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder here.
private fun onPasteShortcutAddress(view: View) {
view.context.alert(R.string.send_alert_shortcut_clicked) {
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
- setAddressError(null)
+ validateAddressInput()
hideKeyboard()
}
}
- override fun updateBalance(old: Long, new: Long) {
- val zecBalance = new / 100000000.0
- val usdBalance = zecBalance * SampleProperties.USD_PER_ZEC
- val availableZecFormatter = DecimalFormat("#.########")
- // TODO: use a formatted string resource here
- val availableTextSpan = "${availableZecFormatter.format(zecBalance)} $zec Available".toSpannable()
- availableTextSpan.setSpan(ForegroundColorSpan(R.color.colorPrimary.toAppColor()), availableTextSpan.length - "Available".length, availableTextSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- availableTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- binding.textZecValueAvailable.text = availableTextSpan
- }
-
private fun onSendZec() {
setSendEnabled(false)
-// val currency = if(zecSelected) "ZEC" else "USD"
-// Toaster.short("sending ${text_value_header.text} $currency...")
-
- //TODO: convert and use only zec amount
-// val amount = text_value_header.text.toString().toDouble()
-// val address = input_zcash_address.text.toString()
- val amount = 0.0018
- val address = "ztestsapling1fg82ar8y8whjfd52l0xcq0w3n7nn7cask2scp9rp27njeurr72ychvud57s9tu90fdqgwdt07lg"
- sendPresenter.sendToAddress(amount, address)
+ sendPresenter.sendFunds()
}
-
- //
- // Internal View Logic
- //
-
- private fun showSendDialog() {
- hideKeyboard()
-
- val address = binding.inputZcashAddress.text
- val headerString = binding.textValueHeader.text.toString()
- val subheaderString = binding.textValueSubheader.text.toString().substringBefore(' ')
- val zecString = if(usdSelected) subheaderString else headerString
- val usdString = if(usdSelected) headerString else subheaderString
- val memo = binding.textAreaMemo.text.toString().trim()
-
- setSendEnabled(false) // partially because we need to lower the button elevation
- binding.dialogTextTitle.text = getString(R.string.send_dialog_title, zecString, zec, usdString)
- binding.dialogTextAddress.text = address
- binding.dialogTextMemoIncluded.visibility = if(memo.isNotEmpty()) View.VISIBLE else View.GONE
- binding.groupDialogSend.visibility = View.VISIBLE
+ private fun exitScanMode() {
+ val cameraFragment = childFragmentManager.findFragmentByTag("camera_fragment")
+ if (cameraFragment != null) {
+ val ft = childFragmentManager.beginTransaction()
+ .remove(cameraFragment)
+ .commit()
+ }
+ binding.buttonCancelScan.visibility = View.GONE
+ binding.groupHiddenDuringScan.visibility = View.VISIBLE
}
private fun hideKeyboard() {
@@ -352,16 +283,116 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
binding.groupDialogSend.visibility = View.GONE
}
+ // note: be careful calling this with `true` that should only happen when all conditions have been validated
private fun setSendEnabled(isEnabled: Boolean) {
binding.buttonSendZec.isEnabled = isEnabled
- if (isEnabled) {
- binding.buttonSendZec.text = "send $zec"
-// binding.progressSend.visibility = View.GONE
+ }
+
+ private fun setAddressError(message: String?) {
+ if (message == null) {
+ setAddressLineColor()
+ binding.textAddressError.text = null
+ binding.textAddressError.visibility = View.GONE
} else {
- binding.buttonSendZec.text = "sending..."
-// binding.progressSend.visibility = View.VISIBLE
+ setAddressLineColor(R.color.zcashRed)
+ binding.textAddressError.text = message
+ binding.textAddressError.visibility = View.VISIBLE
+ setSendEnabled(false)
}
}
+
+ private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
+ DrawableCompat.setTint(
+ binding.inputZcashAddress.background,
+ ContextCompat.getColor(mainActivity, colorRes)
+ )
+ }
+
+ private fun setAmountError(isError: Boolean) {
+ val color = if (isError) R.color.zcashRed else R.color.text_dark
+ binding.textAmountBackground.setTextColor(color.toAppColor())
+ }
+
+
+ //
+ // Validation
+ //
+
+ override fun validateUserInput(): Boolean {
+ val allValid = validateAddressInput() && validateAmountInput() && validateMemo()
+ setSendEnabled(allValid)
+ return allValid
+ }
+
+ /**
+ * Validate the memo input and update presenter when valid.
+ *
+ * @return true when the memo is valid
+ */
+ private fun validateMemo(): Boolean {
+ val memo = binding.textAreaMemo.text.toString()
+ return memo.all { it.isLetterOrDigit() }.also { if (it) sendPresenter.memoValidated(memo) }
+ }
+
+ /**
+ * Validate the address input and update presenter when valid.
+ *
+ * @return true when the address is valid
+ */
+ private fun validateAddressInput(): Boolean {
+ var isValid = false
+ val address = binding.inputZcashAddress.text.toString()
+ if (address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
+ else if (address.any { !it.isLetterOrDigit() }) setAddressError(R.string.send_error_address_invalid_char.toAppString())
+ else setAddressError(null).also { isValid = true; sendPresenter.addressValidated(address) }
+ return isValid
+ }
+
+ /**
+ * Validate the amount input and update the presenter when valid.
+ *
+ * @return true when the amount is valid
+ */
+ private fun validateAmountInput(): Boolean {
+ return try {
+ val amount = binding.textValueHeader.text.toString().safelyConvertToBigDecimal()!!
+ sendPresenter.headerValidated(amount)
+ setAmountError(false)
+ true
+ } catch (t: Throwable) {
+ Toaster.short("Invalid ZEC or USD value")
+ setSendEnabled(false)
+ setAmountError(true)
+ false
+ }
+ }
+
+
+
+
+
+// TODO: come back to this test code later and fix the shared element transitions
+//
+// fun submitWithSharedElements() {
+// var extras = with(binding) {
+// listOf(dialogSendBackground, dialogSendContents, dialogTextTitle, dialogTextAddress)
+// .map{ it to it.transitionName }
+// .let { FragmentNavigatorExtras(*it.toTypedArray()) }
+// }
+// val extras = FragmentNavigatorExtras(
+// binding.dialogSendContents to binding.dialogSendContents.transitionName,
+// binding.dialogTextTitle to getString(R.string.transition_active_transaction_title),
+// binding.dialogTextAddress to getString(R.string.transition_active_transaction_address),
+// binding.dialogSendBackground to getString(R.string.transition_active_transaction_background)
+// )
+//
+// mainActivity.navController.navigate(R.id.nav_home_fragment,
+// null,
+// null,
+// extras)
+// }
+
+
}
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/SendPresenter.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/SendPresenter.kt
index 83b548c..304f971 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/SendPresenter.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/SendPresenter.kt
@@ -1,14 +1,16 @@
package cash.z.android.wallet.ui.presenter
import android.util.Log
+import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.Synchronizer
-import cash.z.wallet.sdk.entity.Transaction
+import cash.z.wallet.sdk.ext.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch
+import java.math.BigDecimal
class SendPresenter(
private val view: SendView,
@@ -17,13 +19,25 @@ class SendPresenter(
interface SendView : PresenterView {
fun updateBalance(old: Long, new: Long)
+ fun setHeaders(isUsdSelected: Boolean, headerString: String, subheaderString: String)
+ fun setHeaderValue(usdString: String)
+ fun setSubheaderValue(usdString: String, isUsdSelected: Boolean)
+ fun showSendDialog(zecString: String, usdString: String, toAddress: String, hasMemo: Boolean)
+ fun validateUserInput(): Boolean
fun submit()
}
private var balanceJob: Job? = null
+ var sendUiModel = SendUiModel()
+
+ //
+ // LifeCycle
+ //
override suspend fun start() {
Log.e("@TWIG-v", "sendPresenter starting!")
+ // set the currency to zec and update the view, intializing everything to zero
+ toggleCurrency()
with(view) {
balanceJob = launchBalanceBinder(synchronizer.balance())
}
@@ -35,27 +49,117 @@ class SendPresenter(
}
fun CoroutineScope.launchBalanceBinder(channel: ReceiveChannel) = launch {
- var old: Long? = null
Log.e("@TWIG-v", "send balance binder starting!")
for (new in channel) {
Log.e("@TWIG-v", "send polled a balance item")
- bind(old, new).also { old = new }
+ bind(new)
}
Log.e("@TWIG-v", "send balance binder exiting!")
}
- fun sendToAddress(value: Double, toAddress: String) {
+
+ //
+ // Public API
+ //
+
+ fun sendFunds() {
//TODO: prehaps grab the activity scope or let the sycnchronizer have scope and make that function not suspend
// also, we need to handle cancellations. So yeah, definitely do this differently
GlobalScope.launch {
- val zatoshi = Math.round(value * 1e8)
- synchronizer.sendToAddress(zatoshi, toAddress)
+ synchronizer.sendToAddress(sendUiModel.zecValue!!, sendUiModel.toAddress)
}
view.submit()
}
- fun bind(old: Long?, new: Long) {
- Log.e("@TWIG-v", "binding balance of $new")
- view.updateBalance(old ?: 0L, new)
+ /**
+ * Called when the user has tapped on the button for toggling currency, swapping zec for usd
+ */
+ fun toggleCurrency() {
+ view.validateUserInput()
+ sendUiModel = sendUiModel.copy(isUsdSelected = !sendUiModel.isUsdSelected)
+ with(sendUiModel) {
+ view.setHeaders(
+ isUsdSelected = isUsdSelected,
+ headerString = if (isUsdSelected) usdValue.toUsdString() else zecValue.convertZatoshiToZecString(),
+ subheaderString = if (isUsdSelected) zecValue.convertZatoshiToZecString() else usdValue.toUsdString()
+ )
+ }
}
-}
+
+ /**
+ * As the user is typing the header string, update the subheader string. Do not modify our own internal model yet.
+ * Internal model is only updated after [headerValidated] is called.
+ */
+ fun headerUpdating(headerValue: String) {
+ headerValue.safelyConvertToBigDecimal()?.let { headerValueAsDecimal ->
+ val subheaderValue = headerValueAsDecimal.convertCurrency(SampleProperties.USD_PER_ZEC, sendUiModel.isUsdSelected)
+
+ // subheader string contains opposite currency of the selected one. so if usd is selected, format the subheader as zec
+ val subheaderString = if(sendUiModel.isUsdSelected) subheaderValue.toZecString() else subheaderValue.toUsdString()
+
+ view.setSubheaderValue(subheaderString, sendUiModel.isUsdSelected)
+ }
+ }
+
+ fun sendPressed() {
+ with(sendUiModel) {
+ view.showSendDialog(
+ zecString = zecValue.convertZatoshiToZecString(),
+ usdString = usdValue.toUsdString(),
+ toAddress = toAddress,
+ hasMemo = !memo.isBlank()
+ )
+ }
+ }
+
+ fun headerValidated(amount: BigDecimal) {
+ with(sendUiModel) {
+ if (isUsdSelected) {
+ val headerString = amount.toUsdString()
+ val usdValue = amount
+ val zecValue = amount.convertUsdToZec(SampleProperties.USD_PER_ZEC)
+ val subheaderString = zecValue.toZecString()
+ sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
+ view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
+ } else {
+ val headerString = amount.toZecString()
+ val zecValue = amount
+ val usdValue = amount.convertZecToUsd(SampleProperties.USD_PER_ZEC)
+ val subheaderString = usdValue.toUsdString()
+ sendUiModel = sendUiModel.copy(zecValue = zecValue.convertZecToZatoshi(), usdValue = usdValue)
+ println("calling setHeaders with $headerString $subheaderString")
+ view.setHeaders(sendUiModel.isUsdSelected, headerString, subheaderString)
+ }
+ }
+ }
+
+ fun addressValidated(address: String) {
+ sendUiModel = sendUiModel.copy(toAddress = address)
+ }
+
+ /**
+ * After the user has typed a memo, validated by the UI, then update the model.
+ *
+ * assert: this method is only called after the memo input has been validated by the UI
+ */
+ fun memoValidated(sanitizedValue: String) {
+ sendUiModel = sendUiModel.copy(memo = sanitizedValue)
+ }
+
+ fun bind(newZecBalance: Long) {
+ if (newZecBalance >= 0) {
+ Log.e("@TWIG-v", "binding balance of $newZecBalance")
+ val old = sendUiModel.zecValue
+ sendUiModel = sendUiModel.copy(zecValue = newZecBalance)
+ view.updateBalance(old ?: 0L, newZecBalance)
+ }
+ }
+
+ data class SendUiModel(
+ val isUsdSelected: Boolean = true,
+ val zecValue: Long? = null,
+ val usdValue: BigDecimal = BigDecimal.ZERO,
+ val toAddress: String = "",
+ val memo: String = ""
+ )
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan.xml b/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan.xml
index 0ee4f52..a7ea557 100644
--- a/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan.xml
+++ b/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan.xml
@@ -1,8 +1,19 @@
-
+
+ android:layout_height="match_parent"
+ app:autoFocus="true"
+ app:facing="back"
+ app:flash="auto">
+
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/layout/fragment_send.xml b/zcash-android-wallet-app/app/src/main/res/layout/fragment_send.xml
index aa6f85a..9877f3e 100644
--- a/zcash-android-wallet-app/app/src/main/res/layout/fragment_send.xml
+++ b/zcash-android-wallet-app/app/src/main/res/layout/fragment_send.xml
@@ -82,14 +82,20 @@
app:layout_constraintTop_toBottomOf="@id/background_header"
app:layout_constraintVertical_chainStyle="spread_inside" />
-
+
+ tools:visibility="invisible"
+ app:layout_constraintEnd_toStartOf="@id/text_value_header"
+ app:layout_constraintBottom_toBottomOf="@id/image_zec_symbol_header"
+ />
@@ -195,7 +205,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_hint_input_zcash_address"
- android:paddingRight="68dp"
+ android:paddingRight="76dp"
android:singleLine="true"
app:backgroundTint="@color/zcashBlack_12"
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
@@ -232,7 +242,7 @@
android:id="@+id/image_address_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginRight="8dp"
+ android:layout_marginRight="16dp"
app:layout_constraintBottom_toBottomOf="@id/image_scan_qr"
app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
app:layout_constraintTop_toTopOf="@id/image_scan_qr"
@@ -307,6 +317,17 @@
app:layout_constraintEnd_toEndOf="@id/divider_memo"
app:layout_constraintTop_toBottomOf="@id/divider_memo" />
+
+
+
@@ -320,7 +341,7 @@
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- tools:visibility="visible" />
+ tools:visibility="gone" />
+ tools:visibility="gone">
+ app:layout_constraintTop_toBottomOf="@id/dialog_text_memo_included"
+ app:layout_goneMarginTop="32dp" />
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/values/strings.xml b/zcash-android-wallet-app/app/src/main/res/values/strings.xml
index 3d3dfb6..02a37aa 100644
--- a/zcash-android-wallet-app/app/src/main/res/values/strings.xml
+++ b/zcash-android-wallet-app/app/src/main/res/values/strings.xml
@@ -65,9 +65,10 @@
Send Zec
Scan QR Code
Paste Sample Address
- 0 %1$s
+ send %1$s
Send %1$s %2$s ($%3$s)?
Paste a valid sample address for testing?
Address is too short.
+ Address contains invalid characters.
diff --git a/zcash-android-wallet-app/app/src/test/java/cash/z/android/wallet/ui/presenter/SendPresenterTest.kt b/zcash-android-wallet-app/app/src/test/java/cash/z/android/wallet/ui/presenter/SendPresenterTest.kt
new file mode 100644
index 0000000..3a56323
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/test/java/cash/z/android/wallet/ui/presenter/SendPresenterTest.kt
@@ -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())
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt
index 7ad15c6..45fbfc6 100644
--- a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt
@@ -16,31 +16,39 @@
package cash.z.android.cameraview
-import cash.z.android.qrecycler.R
import android.app.Activity
import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.media.Image
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
+import android.view.Surface
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.annotation.Nullable
-import androidx.core.os.ParcelableCompat
-import androidx.core.os.ParcelableCompatCreatorCallbacks
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import cash.z.android.cameraview.api21.Camera2
import cash.z.android.cameraview.base.AspectRatio
import cash.z.android.cameraview.base.CameraViewImpl
import cash.z.android.cameraview.base.Constants
import cash.z.android.cameraview.base.PreviewImpl
+import cash.z.android.qrecycler.R
import com.google.android.cameraview.Camera2Api23
import com.google.android.cameraview.TextureViewPreview
-import java.lang.IllegalStateException
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
+import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
+import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
import java.util.*
open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
@@ -56,6 +64,16 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
private var mDisplayOrientationDetector: DisplayOrientationDetector?
+ var firebaseCallback: FirebaseCallback? = null
+ set(value) {
+ (mImpl as? Camera2)?.firebaseCallback = value
+ field = value
+ }
+
+
+ lateinit var cameraId: String
+
+
/**
* @return `true` if the camera is opened.
*/
@@ -293,6 +311,18 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
}
}
+ private var rectPaint = Paint().apply {
+ color = Color.GREEN
+ style = Paint.Style.FILL
+ strokeWidth = 8f
+ }
+
+ override fun draw(canvas: Canvas) {
+ super.draw(canvas)
+ val rect = RectF(0f,0f,canvas.width.toFloat(),canvas.height.toFloat())
+ canvas.drawRect(rect, rectPaint)
+ }
+
override fun onSaveInstanceState(): Parcelable? {
val state = SavedState(super.onSaveInstanceState())
state.facing = facing
@@ -329,6 +359,11 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
// onRestoreInstanceState(state)
// mImpl.start()
}
+
+ // start results in cameraId being set so bubble that up for firebase rotation use
+ when(mImpl) {
+ is Camera2 -> cameraId = (mImpl as Camera2).cameraId!!
+ }
}
/**
@@ -367,6 +402,56 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
mImpl.takePicture()
}
+ fun setBarcode(barcode: FirebaseVisionBarcode) {
+
+ }
+
+ interface FirebaseCallback {
+ fun onImageAvailable(image: Image)
+
+ // TODO: attribute this code. The library I found it in has no attribution but it clearly came from somewhere. Modified it to not require instantiating a sparsearray of orientations (just use when instead) also simplified method signature
+ // one source : https://github.com/firebase/snippets-android/blob/master/mlkit/app/src/main/java/com/google/firebase/example/mlkit/VisionImage.java
+ /**
+ * Get the angle by which an image must be rotated given the device's current
+ * orientation.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @Throws(CameraAccessException::class)
+ fun getRotationCompensation(cameraId: String, activity: Activity): Int {
+ // Get the device's current rotation relative to its "native" orientation.
+ // Then, from the ORIENTATIONS table, look up the angle the image must be
+ // rotated to compensate for the device's rotation.
+ val deviceRotation = activity.windowManager.defaultDisplay.rotation
+ var rotationCompensation = when(deviceRotation) {
+ Surface.ROTATION_0 -> 90
+ Surface.ROTATION_90 -> 0
+ Surface.ROTATION_180 -> 270
+ Surface.ROTATION_270 -> 180
+ else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation")
+ }
+
+ // On most devices, the sensor orientation is 90 degrees, but for some
+ // devices it is 270 degrees. For devices with a sensor orientation of
+ // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
+ val cameraManager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
+ val sensorOrientation = cameraManager
+ .getCameraCharacteristics(cameraId)
+ .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
+ rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
+
+ // Return the corresponding FirebaseVisionImageMetadata rotation value.
+ val result: Int
+ when (rotationCompensation) {
+ 0 -> result = FirebaseVisionImageMetadata.ROTATION_0
+ 90 -> result = FirebaseVisionImageMetadata.ROTATION_90
+ 180 -> result = FirebaseVisionImageMetadata.ROTATION_180
+ 270 -> result = FirebaseVisionImageMetadata.ROTATION_270
+ else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation") // this should be impossible, given that we would have already thrown an exception
+ }
+ return result
+ }
+ }
+
private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
private val mCallbacks = ArrayList()
@@ -465,14 +550,14 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
*
* @param cameraView The associated [CameraView].
*/
- fun onCameraOpened(cameraView: CameraView) {}
+ open fun onCameraOpened(cameraView: CameraView) {}
/**
* Called when camera is closed.
*
* @param cameraView The associated [CameraView].
*/
- fun onCameraClosed(cameraView: CameraView) {}
+ open fun onCameraClosed(cameraView: CameraView) {}
/**
* Called when a picture is taken.
@@ -480,7 +565,7 @@ open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
* @param cameraView The associated [CameraView].
* @param data JPEG data.
*/
- fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
+ open fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
}
companion object {
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api21/Camera2.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api21/Camera2.kt
index 3a6e39a..41ea784 100644
--- a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api21/Camera2.kt
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api21/Camera2.kt
@@ -23,18 +23,24 @@ import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.ImageReader
+import android.os.Handler
import android.util.Log
import android.util.SparseIntArray
import androidx.annotation.NonNull
import androidx.annotation.RequiresPermission
+import cash.z.android.cameraview.CameraView
import cash.z.android.cameraview.base.*
-import java.util.*
+import android.os.HandlerThread
+
+
@TargetApi(21)
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
private val mCameraManager: CameraManager
+ var firebaseCallback: CameraView.FirebaseCallback? = null
+
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
override fun onOpened(@NonNull camera: CameraDevice) {
@@ -70,7 +76,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
- mCaptureCallback, null
+ mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
@@ -101,7 +107,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
)
setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
try {
- mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, null)
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, backgroundHandler)
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
@@ -119,19 +125,21 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
}
private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
+
reader.acquireNextImage().use { image ->
val planes = image.planes
- if (planes.size > 0) {
- val buffer = planes[0].buffer
- val data = ByteArray(buffer.remaining())
- buffer.get(data)
- mCallback.onPictureTaken(data)
+ if (planes.isNotEmpty()) {
+ System.err.println("camoorah : planes was empty: $firebaseCallback")
+ firebaseCallback?.onImageAvailable(image)
+ try{ image.close() } catch(t: Throwable){ System.err.println("camoorah : failed to close")}
+ } else {
+ System.err.println("planes was empty")
}
}
}
- private var mCameraId: String? = null
+ var cameraId: String? = null
private var mCameraCharacteristics: CameraCharacteristics? = null
@@ -167,7 +175,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
- mCaptureCallback, null
+ mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
field = saved
@@ -212,7 +220,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
- mCaptureCallback, null
+ mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
mAutoFocus = !mAutoFocus
@@ -236,6 +244,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
if (!chooseCameraIdByFacing()) {
return false
}
+ startBackgroundThread()
collectCameraInfo()
prepareImageReader()
startOpeningCamera()
@@ -243,6 +252,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
}
override fun stop() {
+ stopBackgroundThread()
if (mCaptureSession != null) {
mCaptureSession!!.close()
mCaptureSession = null
@@ -314,14 +324,14 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
?: throw NullPointerException("Unexpected state: LENS_FACING null")
if (internal == internalFacing) {
- mCameraId = id
+ cameraId = id
mCameraCharacteristics = characteristics
return true
}
}
// Not found
- mCameraId = ids[0]
- mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId!!)
+ cameraId = ids[0]
+ mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId!!)
val level = mCameraCharacteristics!!.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
)
@@ -359,7 +369,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
private fun collectCameraInfo() {
val map = mCameraCharacteristics!!.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
- ) ?: throw IllegalStateException("Failed to get configuration map: " + mCameraId!!)
+ ) ?: throw IllegalStateException("Failed to get configuration map: " + cameraId!!)
mPreviewSizes.clear()
for (size in map.getOutputSizes(mPreview.outputClass)) {
val width = size.width
@@ -392,12 +402,12 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
if (mImageReader != null) {
mImageReader!!.close()
}
- val largest = mPictureSizes.sizes(mAspectRatio).last()
+// val largest = mPictureSizes.sizes(mAspectRatio).last()
+ val previewSize = chooseOptimalSize()
mImageReader = ImageReader.newInstance(
- largest.width, largest.height,
- ImageFormat.JPEG, /* maxImages */ 2
+ previewSize.width / 4, previewSize.height / 4, ImageFormat.YUV_420_888, 2
)
- mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, null)
+ mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, backgroundHandler)
}
/**
@@ -409,9 +419,9 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
@RequiresPermission(Manifest.permission.CAMERA)
private fun startOpeningCamera() {
try {
- mCameraManager.openCamera(mCameraId!!, mCameraDeviceCallback, null)
+ mCameraManager.openCamera(cameraId!!, mCameraDeviceCallback, backgroundHandler)
} catch (e: CameraAccessException) {
- throw RuntimeException("Failed to open camera: " + mCameraId!!, e)
+ throw RuntimeException("Failed to open camera: " + cameraId!!, e)
}
}
@@ -434,9 +444,10 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
try {
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
mPreviewRequestBuilder!!.addTarget(surface)
+ mPreviewRequestBuilder!!.addTarget(mImageReader!!.surface)
mCamera!!.createCaptureSession(
- Arrays.asList(surface, mImageReader!!.surface),
- mSessionCallback, null
+ listOf(surface, mImageReader!!.surface),
+ mSessionCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to start camera session")
@@ -572,7 +583,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
)
try {
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
- mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to lock focus.", e)
}
@@ -583,6 +594,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
* Captures a still picture.
*/
fun captureStillPicture() {
+ Log.e("camoorah", "capturing still picture")
try {
val captureRequestBuilder = mCamera!!.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
@@ -647,7 +659,7 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
) {
unlockFocus()
}
- }, null
+ }, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Cannot capture a still picture.", e)
@@ -665,21 +677,47 @@ internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewI
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
try {
- mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
updateAutoFocus()
updateFlash()
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
)
- mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to restart camera preview.", e)
}
-
}
+ var backgroundHandlerThread: HandlerThread? = null
+ var backgroundHandler: Handler? = null
+
+ /**
+ * Starts a background thread and its [Handler].
+ */
+ private fun startBackgroundThread() {
+ backgroundHandlerThread = HandlerThread("CameraBackgroundProcessor")
+ backgroundHandlerThread?.start()
+ backgroundHandler = Handler(backgroundHandlerThread?.looper)
+ }
+
+ /**
+ * Stops the background thread and its [Handler].
+ */
+ private fun stopBackgroundThread() {
+ backgroundHandlerThread?.quitSafely()
+ try {
+ backgroundHandlerThread?.join()
+ backgroundHandlerThread = null
+ backgroundHandler = null
+ } catch (e: InterruptedException) {
+ e.printStackTrace()
+ }
+ }
+
+
/**
* A [CameraCaptureSession.CaptureCallback] for capturing a still picture.
*/
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeScanningProcessor.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeScanningProcessor.kt
index 723af1c..b742ec3 100644
--- a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeScanningProcessor.kt
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeScanningProcessor.kt
@@ -7,6 +7,7 @@ import com.google.android.gms.tasks.Task
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetector
+import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import java.io.IOException
@@ -18,8 +19,11 @@ class BarcodeScanningProcessor : VisionProcessorBase
// FirebaseVisionBarcodeDetectorOptions.Builder()
// .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
// .build()
- public val detector: FirebaseVisionBarcodeDetector by lazy {
- FirebaseVision.getInstance().visionBarcodeDetector
+ val detector: FirebaseVisionBarcodeDetector by lazy {
+ val options = FirebaseVisionBarcodeDetectorOptions.Builder()
+ .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
+ .build()
+ FirebaseVision.getInstance().getVisionBarcodeDetector(options)
}