diff --git a/zcash-android-wallet-app/app/build.gradle b/zcash-android-wallet-app/app/build.gradle
index 8d2ee68..3a2d97e 100644
--- a/zcash-android-wallet-app/app/build.gradle
+++ b/zcash-android-wallet-app/app/build.gradle
@@ -6,6 +6,7 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'deploygate'
apply plugin: 'com.github.ben-manes.versions'
+apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion buildConfig.compileSdkVersion
@@ -19,19 +20,43 @@ android {
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
}
+
dataBinding {
enabled true
}
+
+ flavorDimensions 'network'
+ productFlavors {
+ // product flavor names cannot start with the word "test" because they would clash with other targets
+ ztestnet {
+ dimension 'network'
+ applicationId 'cash.z.android.wallet.testnet'
+ matchingFallbacks = ['debug']
+ }
+
+ zmainnet {
+ dimension 'network'
+ applicationId 'cash.z.android.wallet.mainnet'
+ matchingFallbacks = ['release']
+ }
+ }
+
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
+ mock {
+ initWith debug
+ matchingFallbacks = ['debug', 'release']
+ }
}
+
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
lintOptions {
lintConfig file("zcash-lint-options.xml")
}
@@ -59,7 +84,7 @@ dependencies {
// Zcash
implementation deps.zcash.walletSdk
- compile project(path: ':qrecycler')
+ implementation project(path: ':qrecycler')
// TODO: get the AAR to provide these
implementation "io.grpc:grpc-okhttp:1.17.1"
@@ -79,6 +104,7 @@ dependencies {
implementation deps.speeddial
implementation deps.lottie
debugImplementation deps.stetho
+ mockImplementation deps.stetho
testImplementation deps.junit
androidTestImplementation deps.androidx.test.runner
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 90cc496..c36c550 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/debug/java/cash/z/android/wallet/di/module/SynchronizerModule.kt b/zcash-android-wallet-app/app/src/debug/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
new file mode 100644
index 0000000..f199391
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/debug/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
@@ -0,0 +1,85 @@
+package cash.z.android.wallet.di.module
+
+import cash.z.android.wallet.BuildConfig
+import cash.z.android.wallet.ZcashWalletApplication
+import cash.z.android.wallet.sample.SampleProperties
+import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT
+import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_SERVER
+import cash.z.wallet.sdk.data.*
+import cash.z.wallet.sdk.jni.JniConverter
+import cash.z.wallet.sdk.secure.Wallet
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/**
+ * Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
+ * application scope.
+ */
+@Module
+internal object SynchronizerModule {
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideTwig(): Twig = TroubleshootingTwig() // troubleshoot on debug, silent on release
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideDownloader(twigger: Twig): CompactBlockStream {
+ return CompactBlockStream(COMPACT_BLOCK_SERVER, COMPACT_BLOCK_PORT, twigger)
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideProcessor(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): CompactBlockProcessor {
+ return CompactBlockProcessor(application, converter, SampleProperties.wallet.cacheDbName, SampleProperties.wallet.dataDbName, logger = twigger)
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideRepository(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): TransactionRepository {
+ return PollingTransactionRepository(application, SampleProperties.wallet.dataDbName, 10_000L, converter, twigger)
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet {
+ return Wallet(converter, application.getDatabasePath(SampleProperties.wallet.dataDbName).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SampleProperties.wallet.seedProvider, spendingKeyProvider = SampleProperties.wallet.spendingKeyProvider, logger = twigger)
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideManager(wallet: Wallet, repository: TransactionRepository, downloader: CompactBlockStream, twigger: Twig): ActiveTransactionManager {
+ return ActiveTransactionManager(repository, downloader.connection, wallet, twigger)
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideJniConverter(): JniConverter {
+ return JniConverter().also {
+ if (BuildConfig.DEBUG) it.initLogs()
+ }
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideSynchronizer(
+ downloader: CompactBlockStream,
+ processor: CompactBlockProcessor,
+ repository: TransactionRepository,
+ manager: ActiveTransactionManager,
+ wallet: Wallet,
+ twigger: Twig
+ ): Synchronizer {
+ return SdkSynchronizer(downloader, processor, repository, manager, wallet, blockPollFrequency = 500_000L, logger = twigger)
+ }
+
+}
diff --git a/zcash-android-wallet-app/app/src/main/AndroidManifest.xml b/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
index 004c0b2..ce71389 100644
--- a/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
+++ b/zcash-android-wallet-app/app/src/main/AndroidManifest.xml
@@ -4,6 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="cash.z.android.wallet">
+
+
+
@@ -27,6 +31,8 @@
+
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/component/ApplicationComponent.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/component/ApplicationComponent.kt
index 2a3eb65..86b9671 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/component/ApplicationComponent.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/component/ApplicationComponent.kt
@@ -30,6 +30,7 @@ import javax.inject.Singleton
ReceiveFragmentModule::class,
RequestFragmentModule::class,
SendFragmentModule::class,
+ ScanFragmentModule::class,
SettingsFragmentModule::class
]
)
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Context.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Context.kt
new file mode 100644
index 0000000..5094d7c
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Context.kt
@@ -0,0 +1,18 @@
+package cash.z.android.wallet.extention
+
+import android.content.Context
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+
+internal inline fun Context.alert(@StringRes messageResId: Int, crossinline block: () -> Unit = {}) {
+ AlertDialog.Builder(this)
+ .setMessage(messageResId)
+ .setPositiveButton(android.R.string.ok) { dialog, _ ->
+ dialog.dismiss()
+ block()
+ }
+ .setNegativeButton(android.R.string.cancel) { dialog, _ ->
+ dialog.dismiss()
+ }
+ .show()
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Int.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Int.kt
index 4b27202..afd6d0f 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Int.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/extention/Int.kt
@@ -2,6 +2,7 @@ package cash.z.android.wallet.extention
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
+import androidx.annotation.IntegerRes
import androidx.annotation.StringRes
import androidx.core.content.res.ResourcesCompat
import cash.z.android.wallet.ZcashWalletApplication
@@ -20,3 +21,10 @@ internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
internal inline fun @receiver:StringRes Int.toAppString(): String {
return ZcashWalletApplication.instance.getString(this)}
+
+/**
+ * Grab an integer from the application resources
+ */
+internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
+ return ZcashWalletApplication.instance.resources.getInteger(this)}
+
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 49b684c..2beefd7 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
@@ -8,6 +8,7 @@ object AliceWallet {
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "testalice_cache.db"
const val dataDbName = "testalice_data8.db"
+ const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed //TODO: add bob's address here
}
object BobWallet {
@@ -17,6 +18,7 @@ object BobWallet {
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "testbob_cache.db"
const val dataDbName = "testbob_data.db"
+ const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed //TODO: add alice's address here
}
object MyWallet {
@@ -26,6 +28,7 @@ object MyWallet {
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "wallet_cache1202.db"
const val dataDbName = "wallet_data1202.db"
+ const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed
}
enum class Servers(val host: String) {
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleQrScanner.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleQrScanner.kt
new file mode 100644
index 0000000..4ab170e
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/sample/SampleQrScanner.kt
@@ -0,0 +1,9 @@
+package cash.z.android.wallet.sample
+
+import cash.z.android.qrecycler.QScanner
+
+class SampleQrScanner : QScanner {
+ override fun scanBarcode(callback: (Result) -> Unit) {
+ callback(Result.success("sampleqrcode_scan_success"))
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ReceiveFragment.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ReceiveFragment.kt
index 466313c..ec1d16c 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ReceiveFragment.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ReceiveFragment.kt
@@ -12,6 +12,7 @@ import cash.z.android.qrecycler.QRecycler
import cash.z.android.wallet.R
import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.util.AddressPartNumberSpan
+import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.jni.JniConverter
import cash.z.wallet.sdk.secure.Wallet
import dagger.Module
@@ -28,7 +29,7 @@ class ReceiveFragment : BaseFragment() {
lateinit var qrecycler: QRecycler
@Inject
- lateinit var wallet: Wallet
+ lateinit var synchronizer: Synchronizer
lateinit var addressParts: Array
@@ -89,7 +90,7 @@ class ReceiveFragment : BaseFragment() {
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
private fun loadAddress(): String {
- return wallet.getAddress()
+ return synchronizer.getAddress()
}
}
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
new file mode 100644
index 0000000..736c3df
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/fragment/ScanFragment.kt
@@ -0,0 +1,185 @@
+package cash.z.android.wallet.ui.fragment
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.databinding.DataBindingUtil
+import cash.z.android.wallet.R
+import cash.z.android.wallet.databinding.FragmentScanBinding
+import cash.z.android.wallet.ui.activity.MainActivity
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+/**
+ * Fragment for scanning addresss, hopefully.
+ */
+class ScanFragment : BaseFragment() {
+
+ lateinit var binding: FragmentScanBinding
+
+// private var cameraSource: CameraSource? = null
+
+ private val requiredPermissions: Array
+ get() {
+ return try {
+ val info = mainActivity.packageManager
+ .getPackageInfo(mainActivity.packageName, PackageManager.GET_PERMISSIONS)
+ val ps = info.requestedPermissions
+ if (ps != null && ps.isNotEmpty()) {
+ ps
+ } else {
+ arrayOfNulls(0)
+ }
+ } catch (e: Exception) {
+ arrayOfNulls(0)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return DataBindingUtil.inflate(
+ inflater, R.layout.fragment_scan, container, false
+ ).let {
+ binding = it
+ it.root
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (activity as MainActivity).let { mainActivity ->
+ mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
+ mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
+ }
+// binding.previewCameraSource.doOnLayout {
+// if (allPermissionsGranted()) {
+// createCameraSource(it.width, it.height)
+// } else {
+// getRuntimePermissions()
+// }
+// }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ if(!allPermissionsGranted()) getRuntimePermissions()
+
+
+// sendPresenter = SendPresenter(this, mainActivity.synchronizer)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if(allPermissionsGranted()) onStartCamera()
+// launch {
+// sendPresenter.start()
+// }
+// startCameraSource()
+ }
+
+ override fun onPause() {
+ binding.cameraView.stop()
+ super.onPause()
+// sendPresenter.stop()
+// binding.previewCameraSource?.stop()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+// cameraSource?.release()
+ }
+
+ /* Camera */
+// private fun createCameraSource(width: Int, height: Int) {
+// Toaster.short("w: $width h: $height")
+// // If there's no existing cameraSource, create one.
+// if (cameraSource == null) {
+// cameraSource = CameraSource(mainActivity, binding.graphicOverlay)
+// }
+//
+// try {
+// cameraSource?.setMachineLearningFrameProcessor(BarcodeScanningProcessor())
+// } catch (e: FirebaseMLException) {
+// Log.e("temporaryBehavior", "can not create camera source")
+// }
+// }
+
+ /**
+ * Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet
+ * (e.g., because onResume was called before the camera source was created), this will be called
+ * again when the camera source is created.
+ */
+// private fun startCameraSource() {
+// cameraSource?.let {
+// try {
+// binding.previewCameraSource?.start(cameraSource!!, binding.graphicOverlay)
+// } catch (e: IOException) {
+// Log.e("temporaryBehavior", "Unable to start camera source.", e)
+// cameraSource?.release()
+// cameraSource = null
+// }
+// }
+// }
+
+ /* Permissions */
+
+ private fun allPermissionsGranted(): Boolean {
+ for (permission in requiredPermissions) {
+ if (!isPermissionGranted(mainActivity, permission!!)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ private fun getRuntimePermissions() {
+ val allNeededPermissions = arrayListOf()
+ for (permission in requiredPermissions) {
+ if (!isPermissionGranted(mainActivity, permission!!)) {
+ allNeededPermissions.add(permission)
+ }
+ }
+
+ if (!allNeededPermissions.isEmpty()) {
+ requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (allPermissionsGranted()) {
+ onStartCamera()
+ }
+ }
+
+ private fun onStartCamera() {
+ with(binding.cameraView) {
+ postDelayed({
+ start()
+ }, 1500L)
+ }
+ }
+
+ companion object {
+ // TODO: continue doing permissions here in a more specific, less general way
+ private const val CAMERA_PERMISSION_REQUEST = 1001
+
+ private fun isPermissionGranted(context: Context, permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+}
+
+
+@Module
+abstract class ScanFragmentModule {
+ @ContributesAndroidInjector
+ abstract fun contributeScanFragment(): ScanFragment
+}
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 f90b866..b9585b0 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
@@ -1,5 +1,6 @@
package cash.z.android.wallet.ui.fragment
+import android.annotation.SuppressLint
import android.graphics.Typeface
import android.os.Bundle
import android.text.Spanned
@@ -9,16 +10,19 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
+import androidx.annotation.ColorRes
+import androidx.appcompat.widget.TooltipCompat
+import androidx.core.content.ContextCompat
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 androidx.transition.TransitionInflater
+import cash.z.android.qrecycler.QScanner
+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.afterTextChanged
-import cash.z.android.wallet.extention.toAppColor
-import cash.z.android.wallet.extention.tryIgnore
+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
@@ -27,6 +31,8 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
import java.text.DecimalFormat
+import javax.inject.Inject
+import kotlin.math.absoluteValue
/**
@@ -35,12 +41,17 @@ import java.text.DecimalFormat
*/
class SendFragment : BaseFragment(), SendPresenter.SendView {
+ @Inject
+ lateinit var qrCodeScanner: QScanner
lateinit var sendPresenter: SendPresenter
lateinit var binding: FragmentSendBinding
private val zecFormatter = DecimalFormat("#.######")
- private val usdFormatter = DecimalFormat("###,###,###.##")
- private val zecSelected get() = binding.groupZecSelected.visibility == View.VISIBLE
+ 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()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -71,35 +82,111 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
}
- initPresenter()
+ init()
initDialog()
}
// temporary function until presenter is setup
- private fun initPresenter() {
+ private fun init() {
binding.imageSwapCurrency.setOnClickListener {
onToggleCurrency()
}
- binding.textValueHeader.afterTextChanged {
- tryIgnore {
- val value = binding.textValueHeader.text.toString().toDouble()
- binding.textValueSubheader.text = if (zecSelected) {
- usdFormatter.format(value * SampleProperties.USD_PER_ZEC)
- } else {
- zecFormatter.format(value / SampleProperties.USD_PER_ZEC)
+ 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.max_memo_length)}"
+ 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() {
@@ -114,7 +201,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
sendPresenter = SendPresenter(this, mainActivity.synchronizer)
- onToggleCurrency()
}
override fun onResume() {
@@ -162,16 +248,51 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
extras)
}
+ @SuppressLint("SetTextI18n")
fun onToggleCurrency() {
- val headerValue = binding.textValueHeader.text
- binding.textValueHeader.setText(binding.textValueSubheader.text)
- binding.textValueSubheader.text = headerValue
- if (zecSelected) {
- binding.groupZecSelected.visibility = View.GONE
- binding.groupUsdSelected.visibility = View.VISIBLE
- } else {
+ 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
+ } else {
+ binding.groupZecSelected.visibility = View.GONE
+ binding.groupUsdSelected.visibility = View.VISIBLE
+ }
+ }
+
+ private fun onScanQrCode(view: View) {
+ hideKeyboard()
+ val fragment = ScanFragment()
+ val ft = childFragmentManager.beginTransaction()
+ .add(R.id.camera_placeholder, fragment, "camera_fragment")
+ .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")
+// }
+// }
+ }
+
+ // 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)
+ hideKeyboard()
}
}
@@ -180,7 +301,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
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()
+ 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
@@ -205,14 +326,27 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
//
private fun showSendDialog() {
- // hide soft keyboard
- mainActivity.getSystemService()
- ?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
+ 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 hideKeyboard() {
+ mainActivity.getSystemService()
+ ?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
+ }
+
private fun hideSendDialog() {
setSendEnabled(true)
binding.groupDialogSend.visibility = View.GONE
@@ -221,7 +355,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
private fun setSendEnabled(isEnabled: Boolean) {
binding.buttonSendZec.isEnabled = isEnabled
if (isEnabled) {
- binding.buttonSendZec.text = "send zec"
+ binding.buttonSendZec.text = "send $zec"
// binding.progressSend.visibility = View.GONE
} else {
binding.buttonSendZec.text = "sending..."
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HistoryPresenter.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HistoryPresenter.kt
index 99b31bd..f15af14 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HistoryPresenter.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HistoryPresenter.kt
@@ -26,7 +26,7 @@ class HistoryPresenter(
override suspend fun start() {
Log.e("@TWIG", "historyPresenter starting!")
- launchTransactionBinder(synchronizer.repository.allTransactions())
+ launchTransactionBinder(synchronizer.allTransactions())
}
override fun stop() {
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HomePresenter.kt b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HomePresenter.kt
index eae45c0..5d825d4 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HomePresenter.kt
+++ b/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/ui/presenter/HomePresenter.kt
@@ -30,9 +30,9 @@ class HomePresenter(
override suspend fun start() {
Log.e("@TWIG-t", "homePresenter starting!")
- launchBalanceBinder(synchronizer.repository.balance())
- launchTransactionBinder(synchronizer.repository.allTransactions())
- launchProgressMonitor(synchronizer.downloader.progress())
+ launchBalanceBinder(synchronizer.balance())
+ launchTransactionBinder(synchronizer.allTransactions())
+ launchProgressMonitor(synchronizer.progress())
launchActiveTransactionMonitor(synchronizer.activeTransactions())
}
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 74eb9ae..83b548c 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
@@ -25,7 +25,7 @@ class SendPresenter(
override suspend fun start() {
Log.e("@TWIG-v", "sendPresenter starting!")
with(view) {
- balanceJob = launchBalanceBinder(synchronizer.repository.balance())
+ balanceJob = launchBalanceBinder(synchronizer.balance())
}
}
@@ -54,10 +54,6 @@ class SendPresenter(
view.submit()
}
- private suspend fun findTransaction(txId: Long): Transaction? {
- return if (txId < 0) null else synchronizer.repository.findTransactionById(txId)
- }
-
fun bind(old: Long?, new: Long) {
Log.e("@TWIG-v", "binding balance of $new")
view.updateBalance(old ?: 0L, new)
diff --git a/zcash-android-wallet-app/app/src/main/res/drawable/background_rounded_corners.xml b/zcash-android-wallet-app/app/src/main/res/drawable/background_rounded_corners.xml
index 2a88bc7..6041399 100644
--- a/zcash-android-wallet-app/app/src/main/res/drawable/background_rounded_corners.xml
+++ b/zcash-android-wallet-app/app/src/main/res/drawable/background_rounded_corners.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/drawable/ic_content_paste_black_24dp.xml b/zcash-android-wallet-app/app/src/main/res/drawable/ic_content_paste_black_24dp.xml
new file mode 100644
index 0000000..a902d9a
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/res/drawable/ic_content_paste_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/zcash-android-wallet-app/app/src/main/res/layout/activity_qr_scanner.xml b/zcash-android-wallet-app/app/src/main/res/layout/activity_qr_scanner.xml
new file mode 100644
index 0000000..cedddff
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/res/layout/activity_qr_scanner.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..0ee4f52
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan_transparent.xml b/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan_transparent.xml
new file mode 100644
index 0000000..8efc66f
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/main/res/layout/fragment_scan_transparent.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 d6c4429..aa6f85a 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
@@ -8,9 +8,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
+ android:isScrollContainer="true"
tools:context=".ui.fragment.SendFragment">
@@ -71,6 +73,7 @@
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:background="@drawable/background_rounded_corners"
+ android:backgroundTint="@color/zcashWhite_40"
app:layout_constraintBottom_toTopOf="@id/input_zcash_address"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintHeight_default="percent"
@@ -79,6 +82,15 @@
app:layout_constraintTop_toBottomOf="@id/background_header"
app:layout_constraintVertical_chainStyle="spread_inside" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/text_value_header"
+ tools:text="0 ZEC" />
+
+
+
+
+
+
@@ -371,6 +409,7 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_background"
+ app:layout_goneMarginTop="32dp"
app:layout_constraintBottom_toTopOf="@+id/dialog_submit_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dialog_text_memo_included" />
@@ -380,7 +419,7 @@
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@null"
- android:text="swipe right to send"
+ android:text="send"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -410,7 +449,6 @@
android:visibility="gone"
app:constraint_referenced_ids="dialog_send_contents, dialog_send_background" />
-
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/main/res/navigation/mobile_navigation.xml b/zcash-android-wallet-app/app/src/main/res/navigation/mobile_navigation.xml
index 6295d1b..b4beaad 100644
--- a/zcash-android-wallet-app/app/src/main/res/navigation/mobile_navigation.xml
+++ b/zcash-android-wallet-app/app/src/main/res/navigation/mobile_navigation.xml
@@ -24,7 +24,11 @@
android:id="@+id/nav_send_fragment"
android:name="cash.z.android.wallet.ui.fragment.SendFragment"
android:label="@string/destination_title_send"
- tools:layout="@layout/fragment_send" />
+ tools:layout="@layout/fragment_send" >
+
+
+
#2196F3
#0D364C66
#FFFFFF
+ #1FFFFFFF
+ #66FFFFFF
+ #A3FFFFFF
#BFFFFFFF
#EDEDED
#CACACA
+ #9B9B9B
#2B2B2B
#1F000000
#66000000
diff --git a/zcash-android-wallet-app/app/src/main/res/values/integers.xml b/zcash-android-wallet-app/app/src/main/res/values/integers.xml
index 68b13bf..a1cb0ba 100644
--- a/zcash-android-wallet-app/app/src/main/res/values/integers.xml
+++ b/zcash-android-wallet-app/app/src/main/res/values/integers.xml
@@ -2,5 +2,6 @@
400
100
- 512
+ 512
+ 78
\ 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 de7e091..3d3dfb6 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
@@ -11,12 +11,15 @@
cancel
cancelled
OK
+ ZECC
+ USD
Send Zcash
Receive Zcash
Request Zcash
+ Scan Address
History
About
Import
@@ -60,5 +63,11 @@
To
Memo (optional)
Send Zec
+ Scan QR Code
+ Paste Sample Address
+ 0 %1$s
+ Send %1$s %2$s ($%3$s)?
+ Paste a valid sample address for testing?
+ Address is too short.
diff --git a/zcash-android-wallet-app/app/src/mock/java/cash/z/android/wallet/di/module/SynchronizerModule.kt b/zcash-android-wallet-app/app/src/mock/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
new file mode 100644
index 0000000..75c485a
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/mock/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
@@ -0,0 +1,36 @@
+package cash.z.android.wallet.di.module
+
+import cash.z.android.qrecycler.QScanner
+import cash.z.android.wallet.sample.SampleQrScanner
+import cash.z.wallet.sdk.data.MockSynchronizer
+import cash.z.wallet.sdk.data.Synchronizer
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/**
+ * Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
+ * application scope.
+ */
+@Module
+internal object SynchronizerModule {
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideQRScanner(): QScanner {
+ // TODO: make an MLKit scanner
+ return SampleQrScanner()
+ }
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ fun provideSynchronizer(): Synchronizer {
+ return MockSynchronizer(
+ transactionInterval = 60_000L,
+ activeTransactionUpdateFrequency = 18_000L,
+ isFirstRun = true
+ )
+ }
+}
diff --git a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/module/SynchronizerModule.kt b/zcash-android-wallet-app/app/src/release/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
similarity index 95%
rename from zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
rename to zcash-android-wallet-app/app/src/release/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
index 4b3914f..d8144fa 100644
--- a/zcash-android-wallet-app/app/src/main/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
+++ b/zcash-android-wallet-app/app/src/release/java/cash/z/android/wallet/di/module/SynchronizerModule.kt
@@ -79,7 +79,7 @@ internal object SynchronizerModule {
wallet: Wallet,
twigger: Twig
): Synchronizer {
- return Synchronizer(downloader, processor, repository, manager, wallet, logger = twigger)
+ return SdkSynchronizer(downloader, processor, repository, manager, wallet, blockPollFrequency = 500_000L, logger = twigger)
}
}
diff --git a/zcash-android-wallet-app/app/src/ztestnet/res/values/integers.xml b/zcash-android-wallet-app/app/src/ztestnet/res/values/integers.xml
new file mode 100644
index 0000000..20a4e17
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/ztestnet/res/values/integers.xml
@@ -0,0 +1,4 @@
+
+
+ 88
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/app/src/ztestnet/res/values/strings.xml b/zcash-android-wallet-app/app/src/ztestnet/res/values/strings.xml
new file mode 100644
index 0000000..d26233f
--- /dev/null
+++ b/zcash-android-wallet-app/app/src/ztestnet/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ TAZ
+
\ No newline at end of file
diff --git a/zcash-android-wallet-app/build.gradle b/zcash-android-wallet-app/build.gradle
index 0ff3659..86261f8 100644
--- a/zcash-android-wallet-app/build.gradle
+++ b/zcash-android-wallet-app/build.gradle
@@ -1,7 +1,7 @@
buildscript {
ext.buildConfig = [
'compileSdkVersion': 28,
- 'minSdkVersion': 16,
+ 'minSdkVersion': 21,
'targetSdkVersion': 28
]
ext.versions = [
@@ -61,6 +61,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "com.github.ben-manes:gradle-versions-plugin:0.20.0"
classpath 'com.deploygate:gradle:1.1.5'
+ classpath 'com.google.gms:google-services:4.2.0'
}
}
diff --git a/zcash-android-wallet-app/qrecycler/build.gradle b/zcash-android-wallet-app/qrecycler/build.gradle
index e7fa069..8433b5f 100644
--- a/zcash-android-wallet-app/qrecycler/build.gradle
+++ b/zcash-android-wallet-app/qrecycler/build.gradle
@@ -27,9 +27,8 @@ android {
dependencies {
implementation deps.androidx.coreKtx
implementation deps.kotlin.stdlib
-
+ api 'com.google.firebase:firebase-ml-vision:19.0.2'
implementation 'com.google.zxing:core:3.2.1'
-
implementation 'com.android.support:appcompat-v7:28.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
diff --git a/zcash-android-wallet-app/qrecycler/src/main/AndroidManifest.xml b/zcash-android-wallet-app/qrecycler/src/main/AndroidManifest.xml
index 2cbf62f..2ef941f 100644
--- a/zcash-android-wallet-app/qrecycler/src/main/AndroidManifest.xml
+++ b/zcash-android-wallet-app/qrecycler/src/main/AndroidManifest.xml
@@ -1,2 +1,5 @@
+ package="cash.z.android.qrecycler" >
+
+
+
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
new file mode 100644
index 0000000..7ad15c6
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview
+
+import cash.z.android.qrecycler.R
+import android.app.Activity
+import android.content.Context
+import android.os.Build
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.AttributeSet
+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.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 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 java.util.*
+
+open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
+ FrameLayout(context, attrs, defStyleAttr) {
+
+ @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)
+
+ internal lateinit var mImpl: CameraViewImpl
+
+ private var mCallbacks: CallbackBridge?
+
+ private var mAdjustViewBounds: Boolean = false
+
+ private var mDisplayOrientationDetector: DisplayOrientationDetector?
+
+ /**
+ * @return `true` if the camera is opened.
+ */
+ val isCameraOpened: Boolean
+ get() = mImpl.isCameraOpened
+
+ /**
+ * @return True when this CameraView is adjusting its bounds to preserve the aspect ratio of
+ * camera.
+ * @see .setAdjustViewBounds
+ */
+ /**
+ * @param adjustViewBounds `true` if you want the CameraView to adjust its bounds to
+ * preserve the aspect ratio of camera.
+ * @see .getAdjustViewBounds
+ */
+ var adjustViewBounds: Boolean
+ get() = mAdjustViewBounds
+ set(adjustViewBounds) {
+ if (mAdjustViewBounds != adjustViewBounds) {
+ mAdjustViewBounds = adjustViewBounds
+ requestLayout()
+ }
+ }
+
+ /**
+ * Gets the direction that the current camera faces.
+ *
+ * @return The camera facing.
+ */
+ /**
+ * Chooses camera by the direction it faces.
+ *
+ * @param facing The camera facing. Must be either [.FACING_BACK] or
+ * [.FACING_FRONT].
+ */
+ var facing: Int
+ @Facing
+ get() = mImpl.facing
+ set(@Facing facing) {
+ mImpl.facing = facing
+ }
+
+ /**
+ * Gets all the aspect ratios supported by the current camera.
+ */
+ val supportedAspectRatios: Set
+ get() = mImpl.supportedAspectRatios
+
+ /**
+ * Gets the current aspect ratio of camera.
+ *
+ * @return The current [AspectRatio]. Can be `null` if no camera is opened yet.
+ */
+ /**
+ * Sets the aspect ratio of camera.
+ *
+ * @param ratio The [AspectRatio] to be set.
+ */
+ var aspectRatio: AspectRatio?
+ @Nullable
+ get() = mImpl.aspectRatio
+ set(@NonNull ratio) {
+ if (mImpl.setAspectRatio(ratio)) {
+ requestLayout()
+ }
+ }
+
+ /**
+ * Returns whether the continuous auto-focus mode is enabled.
+ *
+ * @return `true` if the continuous auto-focus mode is enabled. `false` if it is
+ * disabled, or if it is not supported by the current camera.
+ */
+ /**
+ * Enables or disables the continuous auto-focus mode. When the current camera doesn't support
+ * auto-focus, calling this method will be ignored.
+ *
+ * @param autoFocus `true` to enable continuous auto-focus mode. `false` to
+ * disable it.
+ */
+ var autoFocus: Boolean
+ get() = mImpl.autoFocus
+ set(autoFocus) {
+ mImpl.autoFocus = autoFocus
+ }
+
+ /**
+ * Gets the current flash mode.
+ *
+ * @return The current flash mode.
+ */
+ /**
+ * Sets the flash mode.
+ *
+ * @param flash The desired flash mode.
+ */
+ var flash: Int
+ @Flash
+ get() = mImpl.flash
+ set(@Flash flash) {
+ mImpl.flash = flash
+ }
+
+ /** Direction the camera faces relative to device screen. */
+ @IntDef(FACING_BACK, FACING_FRONT)
+ @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
+ annotation class Facing
+
+ /** The mode for for the camera device's flash control */
+ @IntDef(FLASH_OFF, FLASH_ON, FLASH_TORCH, FLASH_AUTO, FLASH_RED_EYE)
+ annotation class Flash
+
+ init {
+ if (isInEditMode) {
+ mCallbacks = null
+ mDisplayOrientationDetector = null
+ } else {
+ // Internal setup
+ val preview = createPreviewImpl(context)
+ mCallbacks = CallbackBridge()
+ if (Build.VERSION.SDK_INT < 23) {
+ mImpl = Camera2(mCallbacks!!, preview, context)
+ } else {
+ mImpl = Camera2Api23(mCallbacks!!, preview, context)
+ }
+ // Attributes
+ val a = context.obtainStyledAttributes(
+ attrs, R.styleable.CameraView, defStyleAttr,
+ R.style.Widget_CameraView
+ )
+ mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false)
+ facing = a.getInt(R.styleable.CameraView_facing, FACING_BACK)
+ var aspectRatioString = a.getString(R.styleable.CameraView_aspectRatio)
+ if (aspectRatioString != null) {
+ aspectRatio = AspectRatio.parse(aspectRatioString)
+ } else {
+ aspectRatio = Constants.DEFAULT_ASPECT_RATIO
+ }
+ autoFocus = a.getBoolean(R.styleable.CameraView_autoFocus, true)
+ flash = a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO)
+ a.recycle()
+ // Display orientation detector
+ mDisplayOrientationDetector = object : DisplayOrientationDetector(context) {
+ override fun onDisplayOrientationChanged(displayOrientation: Int) {
+ mImpl.setDisplayOrientation(displayOrientation)
+ }
+ }
+ }
+ }
+
+ @NonNull
+ private fun createPreviewImpl(context: Context): PreviewImpl {
+ return TextureViewPreview(context, this)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (!isInEditMode) {
+ mDisplayOrientationDetector!!.enable(ViewCompat.getDisplay(this)!!)
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ if (!isInEditMode) {
+ mDisplayOrientationDetector!!.disable()
+ }
+ super.onDetachedFromWindow()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (isInEditMode) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ return
+ }
+ // Handle android:adjustViewBounds
+ if (mAdjustViewBounds) {
+ if (!isCameraOpened) {
+ mCallbacks!!.reserveRequestLayoutOnOpen()
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ return
+ }
+ val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
+ val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
+ if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) {
+ val ratio = aspectRatio!!
+ var height = (View.MeasureSpec.getSize(widthMeasureSpec) * ratio!!.toFloat()) as Int
+ if (heightMode == View.MeasureSpec.AT_MOST) {
+ height = Math.min(height, View.MeasureSpec.getSize(heightMeasureSpec))
+ }
+ super.onMeasure(
+ widthMeasureSpec,
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
+ )
+ } else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) {
+ val ratio = aspectRatio!!
+ var width = (View.MeasureSpec.getSize(heightMeasureSpec) * ratio!!.toFloat()) as Int
+ if (widthMode == View.MeasureSpec.AT_MOST) {
+ width = Math.min(width, View.MeasureSpec.getSize(widthMeasureSpec))
+ }
+ super.onMeasure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ heightMeasureSpec
+ )
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+ // Measure the TextureView
+ val width = measuredWidth
+ val height = measuredHeight
+ var ratio = aspectRatio
+ if (mDisplayOrientationDetector!!.lastKnownDisplayOrientation % 180 == 0) {
+ ratio = ratio!!.inverse()
+ }
+ assert(ratio != null)
+ if (height < width * ratio!!.y / ratio!!.x) {
+ mImpl.view.measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(
+ width * ratio!!.y / ratio!!.x,
+ View.MeasureSpec.EXACTLY
+ )
+ )
+ } else {
+ mImpl.view.measure(
+ View.MeasureSpec.makeMeasureSpec(
+ height * ratio!!.x / ratio!!.y,
+ View.MeasureSpec.EXACTLY
+ ),
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
+ )
+ }
+ }
+
+ override fun onSaveInstanceState(): Parcelable? {
+ val state = SavedState(super.onSaveInstanceState())
+ state.facing = facing
+ state.ratio = aspectRatio
+ state.autoFocus = autoFocus
+ state.flash = flash
+ return state
+ }
+
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ if (state !is SavedState) {
+ super.onRestoreInstanceState(state)
+ return
+ }
+ val ss = state as SavedState?
+ super.onRestoreInstanceState(ss!!.getSuperState())
+ facing = ss.facing
+ aspectRatio = ss.ratio
+ autoFocus = ss.autoFocus
+ flash = ss.flash
+ }
+
+ /**
+ * Open a camera device and start showing camera preview. This is typically called from
+ * [Activity.onResume].
+ */
+ fun start() {
+ if (!mImpl.start()) {
+ throw IllegalStateException("failed to start even though we're on API 21+")
+// //store the state ,and restore this state after fall back o Camera1
+// val state = onSaveInstanceState()
+// // Camera2 uses legacy hardware layer; fall back to Camera1
+// mImpl = Camera1(mCallbacks, createPreviewImpl(context))
+// onRestoreInstanceState(state)
+// mImpl.start()
+ }
+ }
+
+ /**
+ * Stop camera preview and close the device. This is typically called from
+ * [Activity.onPause].
+ */
+ fun stop() {
+ mImpl.stop()
+ }
+
+ /**
+ * Add a new callback.
+ *
+ * @param callback The [Callback] to add.
+ * @see .removeCallback
+ */
+ fun addCallback(@NonNull callback: Callback) {
+ mCallbacks!!.add(callback)
+ }
+
+ /**
+ * Remove a callback.
+ *
+ * @param callback The [Callback] to remove.
+ * @see .addCallback
+ */
+ fun removeCallback(@NonNull callback: Callback) {
+ mCallbacks!!.remove(callback)
+ }
+
+ /**
+ * Take a picture. The result will be returned to
+ * [Callback.onPictureTaken].
+ */
+ fun takePicture() {
+ mImpl.takePicture()
+ }
+
+ private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
+
+ private val mCallbacks = ArrayList()
+
+ private var mRequestLayoutOnOpen: Boolean = false
+
+ fun add(callback: Callback) {
+ mCallbacks.add(callback)
+ }
+
+ fun remove(callback: Callback) {
+ mCallbacks.remove(callback)
+ }
+
+ override fun onCameraOpened() {
+ if (mRequestLayoutOnOpen) {
+ mRequestLayoutOnOpen = false
+ requestLayout()
+ }
+ for (callback in mCallbacks) {
+ callback.onCameraOpened(this@CameraView)
+ }
+ }
+
+ override fun onCameraClosed() {
+ for (callback in mCallbacks) {
+ callback.onCameraClosed(this@CameraView)
+ }
+ }
+
+ override fun onPictureTaken(data: ByteArray) {
+ for (callback in mCallbacks) {
+ callback.onPictureTaken(this@CameraView, data)
+ }
+ }
+
+ fun reserveRequestLayoutOnOpen() {
+ mRequestLayoutOnOpen = true
+ }
+ }
+
+ protected class SavedState : View.BaseSavedState {
+
+ @Facing
+ internal var facing: Int = 0
+
+ internal var ratio: AspectRatio? = null
+
+ internal var autoFocus: Boolean = false
+
+ @Flash
+ internal var flash: Int = 0
+
+ constructor(source: Parcel) : this(source, AspectRatio::class.java.classLoader!!)
+
+ constructor(source: Parcel, loader: ClassLoader) : super(source) {
+ facing = source.readInt()
+ ratio = source.readParcelable(loader)
+ autoFocus = source.readByte().toInt() != 0
+ flash = source.readInt()
+ }
+
+ constructor(parcelable: Parcelable) : super(parcelable)
+
+ override fun writeToParcel(out: Parcel, flags: Int) {
+ super.writeToParcel(out, flags)
+ out.writeInt(facing)
+ out.writeParcelable(ratio, 0)
+ out.writeByte((if (autoFocus) 1 else 0).toByte())
+ out.writeInt(flash)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): SavedState {
+ return SavedState(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+
+ }
+
+ /**
+ * Callback for monitoring events about [CameraView].
+ */
+ abstract class Callback {
+
+ /**
+ * Called when camera is opened.
+ *
+ * @param cameraView The associated [CameraView].
+ */
+ fun onCameraOpened(cameraView: CameraView) {}
+
+ /**
+ * Called when camera is closed.
+ *
+ * @param cameraView The associated [CameraView].
+ */
+ fun onCameraClosed(cameraView: CameraView) {}
+
+ /**
+ * Called when a picture is taken.
+ *
+ * @param cameraView The associated [CameraView].
+ * @param data JPEG data.
+ */
+ fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
+ }
+
+ companion object {
+
+ /** The camera device faces the opposite direction as the device's screen. */
+ const val FACING_BACK = Constants.FACING_BACK
+
+ /** The camera device faces the same direction as the device's screen. */
+ const val FACING_FRONT = Constants.FACING_FRONT
+
+ /** Flash will not be fired. */
+ const val FLASH_OFF = Constants.FLASH_OFF
+
+ /** Flash will always be fired during snapshot. */
+ const val FLASH_ON = Constants.FLASH_ON
+
+ /** Constant emission of light during preview, auto-focus and snapshot. */
+ const val FLASH_TORCH = Constants.FLASH_TORCH
+
+ /** Flash will be fired automatically when required. */
+ const val FLASH_AUTO = Constants.FLASH_AUTO
+
+ /** Flash will be fired in red-eye reduction mode. */
+ const val FLASH_RED_EYE = Constants.FLASH_RED_EYE
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/DisplayOrientationDetector.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/DisplayOrientationDetector.kt
new file mode 100644
index 0000000..9d1b4cf
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/DisplayOrientationDetector.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview
+
+import android.content.Context
+import android.util.SparseIntArray
+import android.view.Display
+import android.view.OrientationEventListener
+import android.view.Surface
+
+
+/**
+ * Monitors the value returned from [Display.getRotation].
+ */
+internal abstract class DisplayOrientationDetector(context: Context) {
+
+ private val mOrientationEventListener: OrientationEventListener
+
+ var mDisplay: Display? = null
+
+ var lastKnownDisplayOrientation = 0
+ private set
+
+ init {
+ mOrientationEventListener = object : OrientationEventListener(context) {
+
+ /** This is either Surface.Rotation_0, _90, _180, _270, or -1 (invalid). */
+ private var mLastKnownRotation = -1
+
+ override fun onOrientationChanged(orientation: Int) {
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN || mDisplay == null) {
+ return
+ }
+ val rotation = mDisplay!!.rotation
+ if (mLastKnownRotation != rotation) {
+ mLastKnownRotation = rotation
+ dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(rotation))
+ }
+ }
+ }
+ }
+
+ fun enable(display: Display) {
+ mDisplay = display
+ mOrientationEventListener.enable()
+ // Immediately dispatch the first callback
+ dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(display.rotation))
+ }
+
+ fun disable() {
+ mOrientationEventListener.disable()
+ mDisplay = null
+ }
+
+ fun dispatchOnDisplayOrientationChanged(displayOrientation: Int) {
+ lastKnownDisplayOrientation = displayOrientation
+ onDisplayOrientationChanged(displayOrientation)
+ }
+
+ /**
+ * Called when display orientation is changed.
+ *
+ * @param displayOrientation One of 0, 90, 180, and 270.
+ */
+ abstract fun onDisplayOrientationChanged(displayOrientation: Int)
+
+ companion object {
+
+ /** Mapping from Surface.Rotation_n to degrees. */
+ val DISPLAY_ORIENTATIONS = SparseIntArray()
+
+ init {
+ DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0)
+ DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90)
+ DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180)
+ DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270)
+ }
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/TextureViewPreview.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/TextureViewPreview.kt
new file mode 100644
index 0000000..40cd505
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/TextureViewPreview.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.cameraview
+
+
+import cash.z.android.qrecycler.R
+import android.annotation.TargetApi
+import android.content.Context
+import android.graphics.Matrix
+import android.graphics.SurfaceTexture
+import android.view.Surface
+import android.view.TextureView
+import android.view.View
+import android.view.ViewGroup
+import cash.z.android.cameraview.base.PreviewImpl
+
+@TargetApi(14)
+internal class TextureViewPreview(context: Context, parent: ViewGroup) : PreviewImpl() {
+
+ private val mTextureView: TextureView
+
+ private var mDisplayOrientation: Int = 0
+
+ override val surface: Surface
+ get() = Surface(mTextureView.surfaceTexture)
+
+ override val surfaceTexture: SurfaceTexture
+ get() = mTextureView.surfaceTexture
+
+ override val view: View
+ get() = mTextureView
+
+ override val outputClass: Class<*>
+ get() = SurfaceTexture::class.java
+
+ override val isReady: Boolean
+ get() = mTextureView.surfaceTexture != null
+
+ init {
+ val view = View.inflate(context, R.layout.texture_view, parent)
+ mTextureView = view.findViewById(R.id.texture_view) as TextureView
+ mTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
+ setSize(width, height)
+ configureTransform()
+ dispatchSurfaceChanged()
+ }
+
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
+ setSize(width, height)
+ configureTransform()
+ dispatchSurfaceChanged()
+ }
+
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
+ setSize(0, 0)
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
+ }
+ }
+
+ // This method is called only from Camera2.
+ @TargetApi(15)
+ override fun setBufferSize(width: Int, height: Int) {
+ mTextureView.surfaceTexture.setDefaultBufferSize(width, height)
+ }
+
+ override fun setDisplayOrientation(displayOrientation: Int) {
+ mDisplayOrientation = displayOrientation
+ configureTransform()
+ }
+
+ /**
+ * Configures the transform matrix for TextureView based on [.mDisplayOrientation] and
+ * the surface size.
+ */
+ fun configureTransform() {
+ val matrix = Matrix()
+ if (mDisplayOrientation % 180 == 90) {
+ val width = width.toFloat()
+ val height = height.toFloat()
+ // Rotate the camera preview when the screen is landscape.
+ matrix.setPolyToPoly(
+ floatArrayOf(
+ 0f, 0f, // top left
+ width, 0f, // top right
+ 0f, height, // bottom left
+ width, height
+ )// bottom right
+ , 0,
+ if (mDisplayOrientation == 90)
+ // Clockwise
+ floatArrayOf(
+ 0f, height, // top left
+ 0f, 0f, // top right
+ width, height, // bottom left
+ width, 0f
+ )// bottom right
+ else
+ // mDisplayOrientation == 270
+ // Counter-clockwise
+ floatArrayOf(
+ width, 0f, // top left
+ width, height, // top right
+ 0f, 0f, // bottom left
+ 0f, height
+ )// bottom right
+ , 0,
+ 4
+ )
+ } else if (mDisplayOrientation == 180) {
+ matrix.postRotate(180f, width / 2f, height / 2f)
+ }
+ mTextureView.setTransform(matrix)
+ }
+
+}
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
new file mode 100644
index 0000000..3a6e39a
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api21/Camera2.kt
@@ -0,0 +1,788 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.api21
+
+import android.Manifest
+import android.annotation.TargetApi
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.*
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.media.ImageReader
+import android.util.Log
+import android.util.SparseIntArray
+import androidx.annotation.NonNull
+import androidx.annotation.RequiresPermission
+import cash.z.android.cameraview.base.*
+import java.util.*
+
+@TargetApi(21)
+internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
+
+ private val mCameraManager: CameraManager
+
+ private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
+
+ override fun onOpened(@NonNull camera: CameraDevice) {
+ mCamera = camera
+ mCallback.onCameraOpened()
+ startCaptureSession()
+ }
+
+ override fun onClosed(@NonNull camera: CameraDevice) {
+ mCallback.onCameraClosed()
+ }
+
+ override fun onDisconnected(@NonNull camera: CameraDevice) {
+ mCamera = null
+ }
+
+ override fun onError(@NonNull camera: CameraDevice, error: Int) {
+ Log.e(TAG, "onError: " + camera.id + " (" + error + ")")
+ mCamera = null
+ }
+
+ }
+
+ private val mSessionCallback = object : CameraCaptureSession.StateCallback() {
+
+ override fun onConfigured(@NonNull session: CameraCaptureSession) {
+ if (mCamera == null) {
+ return
+ }
+ mCaptureSession = session
+ updateAutoFocus()
+ updateFlash()
+ try {
+ mCaptureSession!!.setRepeatingRequest(
+ mPreviewRequestBuilder!!.build(),
+ mCaptureCallback, null
+ )
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, "Failed to start camera preview.", e)
+ }
+
+ }
+
+ override fun onConfigureFailed(@NonNull session: CameraCaptureSession) {
+ Log.e(TAG, "Failed to configure capture session.")
+ }
+
+ override fun onClosed(@NonNull session: CameraCaptureSession) {
+ if (mCaptureSession != null && mCaptureSession == session) {
+ mCaptureSession = null
+ }
+ }
+
+ }
+
+ private var mCaptureCallback: PictureCaptureCallback = object : PictureCaptureCallback() {
+
+ override fun onPrecaptureRequired() {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
+ setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
+ try {
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, null)
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
+ )
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "Failed to run precapture sequence.", e)
+ }
+
+ }
+
+ override fun onReady() {
+ captureStillPicture()
+ }
+
+ }
+
+ 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)
+ }
+ }
+ }
+
+
+ private var mCameraId: String? = null
+
+ private var mCameraCharacteristics: CameraCharacteristics? = null
+
+ var mCamera: CameraDevice? = null
+
+ var mCaptureSession: CameraCaptureSession? = null
+
+ var mPreviewRequestBuilder: CaptureRequest.Builder? = null
+
+ private var mImageReader: ImageReader? = null
+
+ private val mPreviewSizes = SizeMap()
+
+ private val mPictureSizes = SizeMap()
+
+ private var mFacing: Int = 0
+
+ private var mAspectRatio = Constants.DEFAULT_ASPECT_RATIO
+
+ private var mAutoFocus: Boolean = false
+
+ // Revert
+ override var flash: Int = 0
+ set(flash) {
+ if (this.flash == flash) {
+ return
+ }
+ val saved = this.flash
+ field = flash
+ if (mPreviewRequestBuilder != null) {
+ updateFlash()
+ if (mCaptureSession != null) {
+ try {
+ mCaptureSession!!.setRepeatingRequest(
+ mPreviewRequestBuilder!!.build(),
+ mCaptureCallback, null
+ )
+ } catch (e: CameraAccessException) {
+ field = saved
+ }
+
+ }
+ }
+ }
+
+ private var mDisplayOrientation: Int = 0
+
+ override val isCameraOpened: Boolean
+ get() = mCamera != null
+
+ override var facing: Int
+ get() = mFacing
+ @RequiresPermission(Manifest.permission.CAMERA) set(facing) {
+ if (mFacing == facing) {
+ return
+ }
+ mFacing = facing
+ if (isCameraOpened) {
+ stop()
+ start()
+ }
+ }
+
+ override val supportedAspectRatios: Set
+ get() = mPreviewSizes.ratios()
+
+ // Revert
+ override var autoFocus: Boolean
+ get() = mAutoFocus
+ set(autoFocus) {
+ if (mAutoFocus == autoFocus) {
+ return
+ }
+ mAutoFocus = autoFocus
+ if (mPreviewRequestBuilder != null) {
+ updateAutoFocus()
+ if (mCaptureSession != null) {
+ try {
+ mCaptureSession!!.setRepeatingRequest(
+ mPreviewRequestBuilder!!.build(),
+ mCaptureCallback, null
+ )
+ } catch (e: CameraAccessException) {
+ mAutoFocus = !mAutoFocus
+ }
+
+ }
+ }
+ }
+
+ init {
+ mCameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ mPreview.setCallback(object : PreviewImpl.Callback {
+ override fun onSurfaceChanged() {
+ startCaptureSession()
+ }
+ })
+ }
+
+ @RequiresPermission(Manifest.permission.CAMERA)
+ override fun start(): Boolean {
+ if (!chooseCameraIdByFacing()) {
+ return false
+ }
+ collectCameraInfo()
+ prepareImageReader()
+ startOpeningCamera()
+ return true
+ }
+
+ override fun stop() {
+ if (mCaptureSession != null) {
+ mCaptureSession!!.close()
+ mCaptureSession = null
+ }
+ if (mCamera != null) {
+ mCamera!!.close()
+ mCamera = null
+ }
+ if (mImageReader != null) {
+ mImageReader!!.close()
+ mImageReader = null
+ }
+ }
+
+ override val aspectRatio: AspectRatio get() = mAspectRatio
+
+ override fun setAspectRatio(ratio: AspectRatio?): Boolean {
+ if (ratio == null || ratio == mAspectRatio ||
+ !mPreviewSizes.ratios().contains(ratio)
+ ) {
+ // TODO: Better error handling
+ return false
+ }
+ mAspectRatio = ratio
+ prepareImageReader()
+ if (mCaptureSession != null) {
+ mCaptureSession!!.close()
+ mCaptureSession = null
+ startCaptureSession()
+ }
+ return true
+ }
+
+ override fun takePicture() {
+ if (mAutoFocus) {
+ lockFocus()
+ } else {
+ captureStillPicture()
+ }
+ }
+
+ override fun setDisplayOrientation(displayOrientation: Int) {
+ mDisplayOrientation = displayOrientation
+ mPreview.setDisplayOrientation(mDisplayOrientation)
+ }
+
+ /**
+ *
+ * Chooses a camera ID by the specified camera facing ([.mFacing]).
+ *
+ * This rewrites [.mCameraId], [.mCameraCharacteristics], and optionally
+ * [.mFacing].
+ */
+ private fun chooseCameraIdByFacing(): Boolean {
+ try {
+ val internalFacing = INTERNAL_FACINGS.get(mFacing)
+ val ids = mCameraManager.cameraIdList
+ if (ids.size == 0) { // No camera
+ throw RuntimeException("No camera available.")
+ }
+ for (id in ids) {
+ val characteristics = mCameraManager.getCameraCharacteristics(id)
+ val level = characteristics.get(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+ )
+ if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
+ continue
+ }
+ val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
+ ?: throw NullPointerException("Unexpected state: LENS_FACING null")
+ if (internal == internalFacing) {
+ mCameraId = id
+ mCameraCharacteristics = characteristics
+ return true
+ }
+ }
+ // Not found
+ mCameraId = ids[0]
+ mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId!!)
+ val level = mCameraCharacteristics!!.get(
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+ )
+ if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
+ return false
+ }
+ val internal = mCameraCharacteristics!!.get(CameraCharacteristics.LENS_FACING)
+ ?: throw NullPointerException("Unexpected state: LENS_FACING null")
+ var i = 0
+ val count = INTERNAL_FACINGS.size()
+ while (i < count) {
+ if (INTERNAL_FACINGS.valueAt(i) == internal) {
+ mFacing = INTERNAL_FACINGS.keyAt(i)
+ return true
+ }
+ i++
+ }
+ // The operation can reach here when the only camera device is an external one.
+ // We treat it as facing back.
+ mFacing = Constants.FACING_BACK
+ return true
+ } catch (e: CameraAccessException) {
+ throw RuntimeException("Failed to get a list of camera devices", e)
+ }
+
+ }
+
+ /**
+ *
+ * Collects some information from [.mCameraCharacteristics].
+ *
+ * This rewrites [.mPreviewSizes], [.mPictureSizes], and optionally,
+ * [.mAspectRatio].
+ */
+ private fun collectCameraInfo() {
+ val map = mCameraCharacteristics!!.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ ) ?: throw IllegalStateException("Failed to get configuration map: " + mCameraId!!)
+ mPreviewSizes.clear()
+ for (size in map.getOutputSizes(mPreview.outputClass)) {
+ val width = size.width
+ val height = size.height
+ if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
+ mPreviewSizes.add(Size(width, height))
+ }
+ }
+ mPictureSizes.clear()
+ collectPictureSizes(mPictureSizes, map)
+ for (ratio in mPreviewSizes.ratios()) {
+ if (!mPictureSizes.ratios().contains(ratio)) {
+ mPreviewSizes.remove(ratio)
+ }
+ }
+
+ if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
+ mAspectRatio = mPreviewSizes.ratios().iterator().next()
+ }
+ }
+
+ protected open fun collectPictureSizes(sizes: SizeMap, map: StreamConfigurationMap) {
+ val outputSizes = map.getOutputSizes(ImageFormat.JPEG)
+ for (size in outputSizes) {
+ mPictureSizes.add(Size(size.width, size.height))
+ }
+ }
+
+ private fun prepareImageReader() {
+ if (mImageReader != null) {
+ mImageReader!!.close()
+ }
+ val largest = mPictureSizes.sizes(mAspectRatio).last()
+ mImageReader = ImageReader.newInstance(
+ largest.width, largest.height,
+ ImageFormat.JPEG, /* maxImages */ 2
+ )
+ mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, null)
+ }
+
+ /**
+ *
+ * Starts opening a camera device.
+ *
+ * The result will be processed in [.mCameraDeviceCallback].
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ private fun startOpeningCamera() {
+ try {
+ mCameraManager.openCamera(mCameraId!!, mCameraDeviceCallback, null)
+ } catch (e: CameraAccessException) {
+ throw RuntimeException("Failed to open camera: " + mCameraId!!, e)
+ }
+
+ }
+
+ /**
+ *
+ * Starts a capture session for camera preview.
+ *
+ * This rewrites [.mPreviewRequestBuilder].
+ *
+ * The result will be continuously processed in [.mSessionCallback].
+ */
+ fun startCaptureSession() {
+ if (!isCameraOpened || !mPreview.isReady || mImageReader == null) {
+ return
+ }
+ val previewSize = chooseOptimalSize()
+ mPreview.setBufferSize(previewSize.width, previewSize.height)
+ val surface = mPreview.surface
+ try {
+ mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+ mPreviewRequestBuilder!!.addTarget(surface)
+ mCamera!!.createCaptureSession(
+ Arrays.asList(surface, mImageReader!!.surface),
+ mSessionCallback, null
+ )
+ } catch (e: CameraAccessException) {
+ throw RuntimeException("Failed to start camera session")
+ }
+
+ }
+
+ /**
+ * Chooses the optimal preview size based on [.mPreviewSizes] and the surface size.
+ *
+ * @return The picked size for camera preview.
+ */
+ private fun chooseOptimalSize(): Size {
+ val surfaceLonger: Int
+ val surfaceShorter: Int
+ val surfaceWidth = mPreview.width
+ val surfaceHeight = mPreview.height
+ if (surfaceWidth < surfaceHeight) {
+ surfaceLonger = surfaceHeight
+ surfaceShorter = surfaceWidth
+ } else {
+ surfaceLonger = surfaceWidth
+ surfaceShorter = surfaceHeight
+ }
+ val candidates = mPreviewSizes.sizes(mAspectRatio)
+
+ // Pick the smallest of those big enough
+ for (size in candidates) {
+ if (size.width >= surfaceLonger && size.height >= surfaceShorter) {
+ return size
+ }
+ }
+ // If no size is big enough, pick the largest one.
+ return candidates.last()
+ }
+
+ /**
+ * Updates the internal state of auto-focus to [.mAutoFocus].
+ */
+ fun updateAutoFocus() {
+ if (mAutoFocus) {
+ val modes = mCameraCharacteristics!!.get(
+ CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
+ )
+ // Auto focus is not supported
+ if (modes == null || modes.size == 0 ||
+ modes.size == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF
+ ) {
+ mAutoFocus = false
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_OFF
+ )
+ } else {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+ )
+ }
+ } else {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_OFF
+ )
+ }
+ }
+
+ /**
+ * Updates the internal state of flash to [.mFlash].
+ */
+ fun updateFlash() {
+ when (flash) {
+ Constants.FLASH_OFF -> {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON
+ )
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF
+ )
+ }
+ Constants.FLASH_ON -> {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
+ )
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF
+ )
+ }
+ Constants.FLASH_TORCH -> {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON
+ )
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_TORCH
+ )
+ }
+ Constants.FLASH_AUTO -> {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+ )
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF
+ )
+ }
+ Constants.FLASH_RED_EYE -> {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
+ )
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF
+ )
+ }
+ }
+ }
+
+ /**
+ * Locks the focus as the first step for a still image capture.
+ */
+ private fun lockFocus() {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ try {
+ mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "Failed to lock focus.", e)
+ }
+
+ }
+
+ /**
+ * Captures a still picture.
+ */
+ fun captureStillPicture() {
+ try {
+ val captureRequestBuilder = mCamera!!.createCaptureRequest(
+ CameraDevice.TEMPLATE_STILL_CAPTURE
+ )
+ captureRequestBuilder.addTarget(mImageReader!!.surface)
+ captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ mPreviewRequestBuilder!!.get(CaptureRequest.CONTROL_AF_MODE)
+ )
+ when (flash) {
+ Constants.FLASH_OFF -> {
+ captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON
+ )
+ captureRequestBuilder.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF
+ )
+ }
+ Constants.FLASH_ON -> captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
+ )
+ Constants.FLASH_TORCH -> {
+ captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON
+ )
+ captureRequestBuilder.set(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_TORCH
+ )
+ }
+ Constants.FLASH_AUTO -> captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+ )
+ Constants.FLASH_RED_EYE -> captureRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+ )
+ }
+ // Calculate JPEG orientation.
+ val sensorOrientation = mCameraCharacteristics!!.get(
+ CameraCharacteristics.SENSOR_ORIENTATION
+ )!!
+ captureRequestBuilder.set(
+ CaptureRequest.JPEG_ORIENTATION,
+ (sensorOrientation +
+ mDisplayOrientation * (if (mFacing == Constants.FACING_FRONT) 1 else -1) +
+ 360) % 360
+ )
+ // Stop preview and capture a still picture.
+ mCaptureSession!!.stopRepeating()
+ mCaptureSession!!.capture(captureRequestBuilder.build(),
+ object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ @NonNull session: CameraCaptureSession,
+ @NonNull request: CaptureRequest,
+ @NonNull result: TotalCaptureResult
+ ) {
+ unlockFocus()
+ }
+ }, null
+ )
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "Cannot capture a still picture.", e)
+ }
+
+ }
+
+ /**
+ * Unlocks the auto-focus and restart camera preview. This is supposed to be called after
+ * capturing a still picture.
+ */
+ fun unlockFocus() {
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+ try {
+ mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ updateAutoFocus()
+ updateFlash()
+ mPreviewRequestBuilder!!.set(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_IDLE
+ )
+ mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
+ mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "Failed to restart camera preview.", e)
+ }
+
+ }
+
+ /**
+ * A [CameraCaptureSession.CaptureCallback] for capturing a still picture.
+ */
+ private abstract class PictureCaptureCallback internal constructor() : CameraCaptureSession.CaptureCallback() {
+
+ private var mState: Int = 0
+
+ internal fun setState(state: Int) {
+ mState = state
+ }
+
+ override fun onCaptureProgressed(
+ @NonNull session: CameraCaptureSession,
+ @NonNull request: CaptureRequest, @NonNull partialResult: CaptureResult
+ ) {
+ process(partialResult)
+ }
+
+ override fun onCaptureCompleted(
+ @NonNull session: CameraCaptureSession,
+ @NonNull request: CaptureRequest, @NonNull result: TotalCaptureResult
+ ) {
+ process(result)
+ }
+
+ private fun process(@NonNull result: CaptureResult) {
+ when (mState) {
+ STATE_LOCKING -> {
+ val af = result.get(CaptureResult.CONTROL_AF_STATE)
+ if (af != null) {
+ if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+ val ae = result.get(CaptureResult.CONTROL_AE_STATE)
+ if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+ setState(STATE_CAPTURING)
+ onReady()
+ } else {
+ setState(STATE_LOCKED)
+ onPrecaptureRequired()
+ }
+ }
+ }
+ }
+ STATE_PRECAPTURE -> {
+ val ae = result.get(CaptureResult.CONTROL_AE_STATE)
+ if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
+ ae == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED ||
+ ae == CaptureResult.CONTROL_AE_STATE_CONVERGED
+ ) {
+ setState(STATE_WAITING)
+ }
+ }
+ STATE_WAITING -> {
+ val ae = result.get(CaptureResult.CONTROL_AE_STATE)
+ if (ae == null || ae != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
+ setState(STATE_CAPTURING)
+ onReady()
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when it is ready to take a still picture.
+ */
+ abstract fun onReady()
+
+ /**
+ * Called when it is necessary to run the precapture sequence.
+ */
+ abstract fun onPrecaptureRequired()
+
+ companion object {
+
+ internal val STATE_PREVIEW = 0
+ internal val STATE_LOCKING = 1
+ internal val STATE_LOCKED = 2
+ internal val STATE_PRECAPTURE = 3
+ internal val STATE_WAITING = 4
+ internal val STATE_CAPTURING = 5
+ }
+
+ }
+
+ companion object {
+
+ private val TAG = "Camera2"
+
+ private val INTERNAL_FACINGS = SparseIntArray()
+
+ init {
+ INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK)
+ INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT)
+ }
+
+ /**
+ * Max preview width that is guaranteed by Camera2 API
+ */
+ private val MAX_PREVIEW_WIDTH = 1920
+
+ /**
+ * Max preview height that is guaranteed by Camera2 API
+ */
+ private val MAX_PREVIEW_HEIGHT = 1080
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api23/Camera2Api23.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api23/Camera2Api23.kt
new file mode 100644
index 0000000..42ed7a1
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/api23/Camera2Api23.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.cameraview
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.graphics.ImageFormat
+import android.hardware.camera2.params.StreamConfigurationMap
+import cash.z.android.cameraview.CameraView
+import cash.z.android.cameraview.api21.Camera2
+import cash.z.android.cameraview.base.CameraViewImpl
+import cash.z.android.cameraview.base.PreviewImpl
+import cash.z.android.cameraview.base.Size
+import cash.z.android.cameraview.base.SizeMap
+
+
+@TargetApi(23)
+internal class Camera2Api23(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) :
+ Camera2(callback, preview, context) {
+
+ protected override fun collectPictureSizes(sizes: SizeMap, map: StreamConfigurationMap) {
+ // Try to get hi-res output sizes
+ val outputSizes = map.getHighResolutionOutputSizes(ImageFormat.JPEG)
+ if (outputSizes != null) {
+ for (size in map.getHighResolutionOutputSizes(ImageFormat.JPEG)) {
+ sizes.add(Size(size.width, size.height))
+ }
+ }
+ if (sizes.isEmpty) {
+ super.collectPictureSizes(sizes, map)
+ }
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/AspectRatio.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/AspectRatio.kt
new file mode 100644
index 0000000..fcf7681
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/AspectRatio.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.annotation.NonNull
+import androidx.collection.SparseArrayCompat
+
+/**
+ * Immutable class for describing proportional relationship between width and height.
+ */
+class AspectRatio private constructor(val x: Int, val y: Int) : Comparable, Parcelable {
+
+ constructor(parcel: Parcel) : this(
+ parcel.readInt(),
+ parcel.readInt()
+ ) {
+ }
+
+ fun matches(size: Size): Boolean {
+ val gcd = gcd(size.width, size.height)
+ val x = size.width / gcd
+ val y = size.height / gcd
+ return this.x == x && this.y == y
+ }
+
+ override fun equals(o: Any?): Boolean {
+ if (o == null) {
+ return false
+ }
+ if (this === o) {
+ return true
+ }
+ if (o is AspectRatio) {
+ val ratio = o as AspectRatio?
+ return x == ratio!!.x && y == ratio.y
+ }
+ return false
+ }
+
+ override fun toString(): String {
+ return x.toString() + ":" + y
+ }
+
+ fun toFloat(): Float {
+ return x.toFloat() / y
+ }
+
+ override fun hashCode(): Int {
+ // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
+ return y xor (x shl Integer.SIZE / 2 or x.ushr(Integer.SIZE / 2))
+ }
+
+ override fun compareTo(@NonNull another: AspectRatio): Int {
+ if (equals(another)) {
+ return 0
+ } else if (toFloat() - another.toFloat() > 0) {
+ return 1
+ }
+ return -1
+ }
+
+ /**
+ * @return The inverse of this [AspectRatio].
+ */
+ fun inverse(): AspectRatio {
+
+ return AspectRatio.of(y, x)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeInt(x)
+ dest.writeInt(y)
+ }
+ companion object CREATOR : Parcelable.Creator {
+
+ private val sCache = SparseArrayCompat>(16)
+
+ /**
+ * Returns an instance of [AspectRatio] specified by `x` and `y` values.
+ * The values `x` and `` will be reduced by their greatest common divider.
+ *
+ * @param x The width
+ * @param y The height
+ * @return An instance of [AspectRatio]
+ */
+ fun of(inX: Int, inY: Int): AspectRatio {
+ var x = inX
+ var y = inY
+ val gcd = gcd(x, y)
+ x /= gcd
+ y /= gcd
+ var arrayX: SparseArrayCompat? = sCache.get(x)
+ if (arrayX == null) {
+ val ratio = AspectRatio(x, y)
+ arrayX = SparseArrayCompat()
+ arrayX!!.put(y, ratio)
+ sCache.put(x, arrayX)
+ return ratio
+ } else {
+ var ratio = arrayX!!.get(y)
+ if (ratio == null) {
+ ratio = AspectRatio(x, y)
+ arrayX!!.put(y, ratio)
+ }
+ return ratio
+ }
+ }
+
+ /**
+ * Parse an [AspectRatio] from a [String] formatted like "4:3".
+ *
+ * @param s The string representation of the aspect ratio
+ * @return The aspect ratio
+ * @throws IllegalArgumentException when the format is incorrect.
+ */
+ fun parse(s: String): AspectRatio {
+ val position = s.indexOf(':')
+ if (position == -1) {
+ throw IllegalArgumentException("Malformed aspect ratio: $s")
+ }
+ try {
+ val x = Integer.parseInt(s.substring(0, position))
+ val y = Integer.parseInt(s.substring(position + 1))
+ return AspectRatio.of(x, y)
+ } catch (e: NumberFormatException) {
+ throw IllegalArgumentException("Malformed aspect ratio: $s", e)
+ }
+
+ }
+
+ private fun gcd(a: Int, b: Int): Int {
+ var a = a
+ var b = b
+ while (b != 0) {
+ val c = b
+ b = a % b
+ a = c
+ }
+ return a
+ }
+ override fun createFromParcel(parcel: Parcel): AspectRatio {
+ return AspectRatio(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/CameraViewImpl.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/CameraViewImpl.kt
new file mode 100644
index 0000000..4e42a99
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/CameraViewImpl.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+import android.view.View
+
+internal abstract class CameraViewImpl(protected val mCallback: Callback, protected val mPreview: PreviewImpl) {
+
+ val view: View
+ get() = mPreview.view
+
+ internal abstract val isCameraOpened: Boolean
+
+ internal abstract var facing: Int
+
+ internal abstract val supportedAspectRatios: Set
+
+ internal abstract val aspectRatio: AspectRatio
+
+ internal abstract var autoFocus: Boolean
+
+ internal abstract var flash: Int
+
+ /**
+ * @return `true` if the implementation was able to start the camera session.
+ */
+ internal abstract fun start(): Boolean
+
+ internal abstract fun stop()
+
+ /**
+ * @return `true` if the aspect ratio was changed.
+ */
+ internal abstract fun setAspectRatio(ratio: AspectRatio?): Boolean
+
+ internal abstract fun takePicture()
+
+ internal abstract fun setDisplayOrientation(displayOrientation: Int)
+
+ internal interface Callback {
+
+ fun onCameraOpened()
+
+ fun onCameraClosed()
+
+ fun onPictureTaken(data: ByteArray)
+
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Constants.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Constants.kt
new file mode 100644
index 0000000..5911b0d
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Constants.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+
+interface Constants {
+ companion object {
+
+ val DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3)
+
+ const val FACING_BACK = 0
+ const val FACING_FRONT = 1
+
+ const val FLASH_OFF = 0
+ const val FLASH_ON = 1
+ const val FLASH_TORCH = 2
+ const val FLASH_AUTO = 3
+ const val FLASH_RED_EYE = 4
+
+ const val LANDSCAPE_90 = 90
+ const val LANDSCAPE_270 = 270
+ }
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/PreviewImpl.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/PreviewImpl.kt
new file mode 100644
index 0000000..815a3fd
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/PreviewImpl.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.View
+
+
+/**
+ * Encapsulates all the operations related to camera preview in a backward-compatible manner.
+ */
+internal abstract class PreviewImpl {
+
+ private var mCallback: Callback? = null
+
+ var width: Int = 0
+ private set
+
+ var height: Int = 0
+ private set
+
+ internal abstract val surface: Surface
+
+ internal abstract val view: View
+
+ internal abstract val outputClass: Class<*>
+
+ internal abstract val isReady: Boolean
+
+ val surfaceHolder: SurfaceHolder?
+ get() = null
+
+ open val surfaceTexture: Any?
+ get() = null
+
+ internal interface Callback {
+ fun onSurfaceChanged()
+ }
+
+ fun setCallback(callback: Callback) {
+ mCallback = callback
+ }
+
+ internal abstract fun setDisplayOrientation(displayOrientation: Int)
+
+ protected fun dispatchSurfaceChanged() {
+ mCallback!!.onSurfaceChanged()
+ }
+
+ open fun setBufferSize(width: Int, height: Int) {}
+
+ fun setSize(width: Int, height: Int) {
+ this.width = width
+ this.height = height
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Size.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Size.kt
new file mode 100644
index 0000000..d1c97bc
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/Size.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+import androidx.annotation.NonNull
+
+
+/**
+ * Immutable class for describing width and height dimensions in pixels.
+ */
+
+/**
+ * Create a new immutable Size instance.
+ *
+ * @param width The width of the size, in pixels
+ * @param height The height of the size, in pixels
+ */
+class Size(val width: Int, val height: Int) : Comparable {
+
+ override fun equals(o: Any?): Boolean {
+ if (o == null) {
+ return false
+ }
+ if (this === o) {
+ return true
+ }
+ if (o is Size) {
+ val size = o as Size?
+ return width == size!!.width && height == size.height
+ }
+ return false
+ }
+
+ override fun toString(): String {
+ return width.toString() + "x" + height
+ }
+
+ override fun hashCode(): Int {
+ // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
+ return height xor (width shl Integer.SIZE / 2 or width.ushr(Integer.SIZE / 2))
+ }
+
+ override fun compareTo(@NonNull another: Size): Int {
+ return width * height - another.width * another.height
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/SizeMap.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/SizeMap.kt
new file mode 100644
index 0000000..282d079
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/base/SizeMap.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cash.z.android.cameraview.base
+
+
+import androidx.collection.ArrayMap
+import java.util.SortedSet
+import java.util.TreeSet
+
+/**
+ * A collection class that automatically groups [Size]s by their [AspectRatio]s.
+ */
+internal class SizeMap {
+
+ private val mRatios = ArrayMap>()
+
+ val isEmpty: Boolean
+ get() = mRatios.isEmpty()
+
+ /**
+ * Add a new [Size] to this collection.
+ *
+ * @param size The size to add.
+ * @return `true` if it is added, `false` if it already exists and is not added.
+ */
+ fun add(size: Size): Boolean {
+ for (ratio in mRatios.keys) {
+ if (ratio.matches(size)) {
+ val sizes = mRatios[ratio]
+ if (sizes?.contains(size) == true) {
+ return false
+ } else {
+ if(sizes == null) mRatios[ratio] = TreeSet().let { it.add(size); it }
+ else sizes.add(size)
+ return true
+ }
+ }
+ }
+ // None of the existing ratio matches the provided size; add a new key
+ val sizes = TreeSet()
+ sizes.add(size)
+ mRatios.put(AspectRatio.of(size.width, size.height), sizes)
+ return true
+ }
+
+ /**
+ * Removes the specified aspect ratio and all sizes associated with it.
+ *
+ * @param ratio The aspect ratio to be removed.
+ */
+ fun remove(ratio: AspectRatio) {
+ mRatios.remove(ratio)
+ }
+
+ fun ratios(): Set {
+ return mRatios.keys
+ }
+
+ fun sizes(ratio: AspectRatio): SortedSet {
+ return mRatios[ratio]!!
+ }
+
+ fun clear() {
+ mRatios.clear()
+ }
+
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/qrecycler/QScanner.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/qrecycler/QScanner.kt
new file mode 100644
index 0000000..738c330
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/qrecycler/QScanner.kt
@@ -0,0 +1,8 @@
+package cash.z.android.qrecycler
+
+/**
+ * An interface to allow for plugging in any scanner
+ */
+interface QScanner {
+ fun scanBarcode(callback: (Result) -> Unit)
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeGraphic.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeGraphic.kt
new file mode 100644
index 0000000..5c7bca9
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeGraphic.kt
@@ -0,0 +1,47 @@
+package cash.z.android.wallet.ui.util.vision
+
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
+
+class BarcodeGraphic(overlay: GraphicOverlay, private val barcode: FirebaseVisionBarcode) :
+ GraphicOverlay.Graphic(overlay) {
+
+ private var rectPaint = Paint().apply {
+ color = TEXT_COLOR
+ style = Paint.Style.STROKE
+ strokeWidth = STROKE_WIDTH
+ }
+
+ private var barcodePaint = Paint().apply {
+ color = TEXT_COLOR
+ textSize = TEXT_SIZE
+ }
+
+ /**
+ * Draws the barcode block annotations for position, size, and raw value on the supplied canvas.
+ */
+ override fun draw(canvas: Canvas) {
+ // Draws the bounding box around the BarcodeBlock.
+ val rect = RectF(barcode.boundingBox)
+ rect.left = translateX(rect.left)
+ rect.top = translateY(rect.top)
+ rect.right = translateX(rect.right)
+ rect.bottom = translateY(rect.bottom)
+ canvas.drawRect(rect, rectPaint)
+
+ // Renders the barcode at the bottom of the box.
+ barcode.rawValue?.let { value ->
+ canvas.drawText(value, rect.left, rect.bottom, barcodePaint)
+ }
+ }
+
+ companion object {
+ private const val TEXT_COLOR = Color.WHITE
+ private const val TEXT_SIZE = 54.0f
+ private const val STROKE_WIDTH = 4.0f
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..723af1c
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BarcodeScanningProcessor.kt
@@ -0,0 +1,66 @@
+package cash.z.android.wallet.ui.util.vision
+
+import android.graphics.Bitmap
+import android.util.Log
+import cash.z.android.wallet.ui.util.VisionProcessorBase
+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.common.FirebaseVisionImage
+import java.io.IOException
+
+/** Barcode Detector Demo. */
+class BarcodeScanningProcessor : VisionProcessorBase>() {
+
+ // Note that if you know which format of barcode your app is dealing with, detection will be
+ // faster to specify the supported barcode formats one by one, e.g.
+ // FirebaseVisionBarcodeDetectorOptions.Builder()
+ // .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
+ // .build()
+ public val detector: FirebaseVisionBarcodeDetector by lazy {
+ FirebaseVision.getInstance().visionBarcodeDetector
+ }
+
+
+ override fun stop() {
+ try {
+ detector.close()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception thrown while trying to close Barcode Detector: $e")
+ }
+ }
+
+ override fun detectInImage(image: FirebaseVisionImage): Task> {
+ return detector.detectInImage(image)
+ }
+
+ override fun onSuccess(
+ originalCameraImage: Bitmap?,
+ barcodes: List,
+ frameMetadata: FrameMetadata,
+ graphicOverlay: GraphicOverlay
+ ) {
+ graphicOverlay.clear()
+
+ originalCameraImage?.let {
+ val imageGraphic = CameraImageGraphic(graphicOverlay, it)
+ graphicOverlay.add(imageGraphic)
+ }
+
+ barcodes.forEach {
+ val barcodeGraphic = BarcodeGraphic(graphicOverlay, it)
+ graphicOverlay.add(barcodeGraphic)
+ }
+ graphicOverlay.postInvalidate()
+ }
+
+ override fun onFailure(e: Exception) {
+ Log.e(TAG, "Barcode detection failed $e")
+ }
+
+ companion object {
+
+ private const val TAG = "BarcodeScanProc"
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BitmapUtils.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BitmapUtils.kt
new file mode 100644
index 0000000..c34f5e5
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/BitmapUtils.kt
@@ -0,0 +1,75 @@
+package cash.z.android.wallet.ui.util.vision
+
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+import android.graphics.*
+import android.hardware.Camera.CameraInfo
+import android.util.Log
+import androidx.annotation.Nullable
+import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
+import java.io.ByteArrayOutputStream
+import java.nio.ByteBuffer
+
+/** Utils functions for bitmap conversions. */
+object BitmapUtils {
+
+ // Convert NV21 format byte buffer to bitmap.
+ @Nullable
+ fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? {
+ data.rewind()
+ val imageInBuffer = ByteArray(data.limit())
+ data.get(imageInBuffer, 0, imageInBuffer.size)
+ try {
+ val image = YuvImage(
+ imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null
+ )
+ if (image != null) {
+ val stream = ByteArrayOutputStream()
+ image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream)
+
+ val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
+
+ stream.close()
+ return rotateBitmap(
+ bmp,
+ metadata.rotation,
+ metadata.cameraFacing
+ )
+ }
+ } catch (e: Exception) {
+ Log.e("VisionProcessorBase", "Error: " + e.message)
+ }
+
+ return null
+ }
+
+ // Rotates a bitmap if it is converted from a bytebuffer.
+ private fun rotateBitmap(bitmap: Bitmap, rotation: Int, facing: Int): Bitmap {
+ val matrix = Matrix()
+ var rotationDegree = 0
+ when (rotation) {
+ FirebaseVisionImageMetadata.ROTATION_90 -> rotationDegree = 90
+ FirebaseVisionImageMetadata.ROTATION_180 -> rotationDegree = 180
+ FirebaseVisionImageMetadata.ROTATION_270 -> rotationDegree = 270
+ else -> {
+ }
+ }
+
+ // Rotate the image back to straight.}
+ matrix.postRotate(rotationDegree.toFloat())
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ }
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraImageGraphic.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraImageGraphic.kt
new file mode 100644
index 0000000..607cd09
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraImageGraphic.kt
@@ -0,0 +1,15 @@
+package cash.z.android.wallet.ui.util.vision
+
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import cash.z.android.wallet.ui.util.vision.GraphicOverlay.Graphic
+
+/** Draw camera image to background. */
+class CameraImageGraphic(overlay: GraphicOverlay, private val bitmap: Bitmap) : Graphic(overlay) {
+
+ override fun draw(canvas: Canvas) {
+ canvas.drawBitmap(bitmap, null, Rect(0, 0, canvas.width, canvas.height), null)
+ }
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSource.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSource.kt
new file mode 100644
index 0000000..bd8328e
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSource.kt
@@ -0,0 +1,723 @@
+package cash.z.android.wallet.ui.util.vision
+
+// Copyright 2019 Electric Coin Company
+//
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// This file has been modified by Electric Coin Company to allow for
+// better fitting previews.
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
+import android.hardware.Camera
+import android.util.Log
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.WindowManager
+import androidx.annotation.Nullable
+import androidx.annotation.RequiresPermission
+import cash.z.android.wallet.ui.util.VisionImageProcessor
+import com.google.android.gms.common.images.Size
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.util.*
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+@SuppressLint("MissingPermission")
+class CameraSource(protected var activity: Activity, private val graphicOverlay: GraphicOverlay, private val requestedPreviewWidth: Int = 480, private val requestedPreviewHeight: Int = 360) {
+
+ private var camera: Camera? = null
+
+ /**
+ * Returns the selected camera; one of [.CAMERA_FACING_BACK] or [ ][.CAMERA_FACING_FRONT].
+ */
+ var cameraFacing = CAMERA_FACING_BACK
+ protected set
+
+ /**
+ * Rotation of the device, and thus the associated preview images captured from the device. See
+ * Frame.Metadata#getRotation().
+ */
+ private var rotation: Int = 0
+
+ /** Returns the preview size that is currently in use by the underlying camera. */
+ var previewSize: Size? = null
+ private set
+
+ // These values may be requested by the caller. Due to hardware limitations, we may need to
+ // select close, but not exactly the same values for these.
+ private val requestedFps = 20.0f
+ private val requestedAutoFocus = true
+
+ // These instances need to be held onto to avoid GC of their underlying resources. Even though
+ // these aren't used outside of the method that creates them, they still must have hard
+ // references maintained to them.
+ private var dummySurfaceTexture: SurfaceTexture? = null
+
+ // True if a SurfaceTexture is being used for the preview, false if a SurfaceHolder is being
+ // used for the preview. We want to be compatible back to Gingerbread, but SurfaceTexture
+ // wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the
+ // developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't
+ // want to display a preview we use a SurfaceTexture if we are running at least Honeycomb.
+ private var usingSurfaceTexture: Boolean = false
+
+ /**
+ * Dedicated thread and associated runnable for calling into the detector with frames, as the
+ * frames become available from the camera.
+ */
+ private var processingThread: Thread? = null
+
+ private val processingRunnable: FrameProcessingRunnable
+
+ private val processorLock = Any()
+ // @GuardedBy("processorLock")
+ private var frameProcessor: VisionImageProcessor? = null
+
+ /**
+ * Map to convert between a byte array, received from the camera, and its associated byte buffer.
+ * We use byte buffers internally because this is a more efficient way to call into native code
+ * later (avoids a potential copy).
+ *
+ *
+ * **Note:** uses IdentityHashMap here instead of HashMap because the behavior of an array's
+ * equals, hashCode and toString methods is both useless and unexpected. IdentityHashMap enforces
+ * identity ('==') check on the keys.
+ */
+ private val bytesToByteBuffer = IdentityHashMap()
+
+ init {
+ graphicOverlay.clear()
+ processingRunnable = FrameProcessingRunnable()
+
+ if (Camera.getNumberOfCameras() == 1) {
+ val cameraInfo = Camera.CameraInfo()
+ Camera.getCameraInfo(0, cameraInfo)
+ cameraFacing = cameraInfo.facing
+ }
+ }
+
+ // ==============================================================================================
+ // Public
+ // ==============================================================================================
+
+ /** Stops the camera and releases the resources of the camera and underlying detector. */
+ fun release() {
+ synchronized(processorLock) {
+ stop()
+ processingRunnable.release()
+ cleanScreen()
+
+ if (frameProcessor != null) {
+ frameProcessor!!.stop()
+ }
+ }
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The preview
+ * frames are not displayed.
+ *
+ * @throws IOException if the camera's preview texture or display could not be initialized
+ */
+ @SuppressLint("MissingPermission")
+ @RequiresPermission(Manifest.permission.CAMERA)
+ @Synchronized
+ @Throws(IOException::class)
+ fun start(): CameraSource {
+ if (camera != null) {
+ return this
+ }
+
+ camera = createCamera()
+ dummySurfaceTexture = SurfaceTexture(DUMMY_TEXTURE_NAME)
+ camera!!.setPreviewTexture(dummySurfaceTexture)
+ usingSurfaceTexture = true
+ camera!!.startPreview()
+
+ processingThread = Thread(processingRunnable)
+ processingRunnable.setActive(true)
+ processingThread!!.start()
+ return this
+ }
+
+ /**
+ * Opens the camera and starts sending preview frames to the underlying detector. The supplied
+ * surface holder is used for the preview so frames can be displayed to the user.
+ *
+ * @param surfaceHolder the surface holder to use for the preview frames
+ * @throws IOException if the supplied surface holder could not be used as the preview display
+ */
+ @RequiresPermission(Manifest.permission.CAMERA)
+ @Synchronized
+ @Throws(IOException::class)
+ fun start(surfaceHolder: SurfaceHolder): CameraSource {
+ if (camera != null) {
+ return this
+ }
+
+ camera = createCamera()
+ camera!!.setPreviewDisplay(surfaceHolder)
+ camera!!.startPreview()
+
+ processingThread = Thread(processingRunnable)
+ processingRunnable.setActive(true)
+ processingThread!!.start()
+
+ usingSurfaceTexture = false
+ return this
+ }
+
+ /**
+ * Closes the camera and stops sending frames to the underlying frame detector.
+ *
+ *
+ * This camera source may be restarted again by calling [.start] or [ ][.start].
+ *
+ *
+ * Call [.release] instead to completely shut down this camera source and release the
+ * resources of the underlying detector.
+ */
+ @Synchronized
+ fun stop() {
+ processingRunnable.setActive(false)
+ if (processingThread != null) {
+ try {
+ // Wait for the thread to complete to ensure that we can't have multiple threads
+ // executing at the same time (i.e., which would happen if we called start too
+ // quickly after stop).
+ processingThread!!.join()
+ } catch (e: InterruptedException) {
+ Log.d(TAG, "Frame processing thread interrupted on release.")
+ }
+
+ processingThread = null
+ }
+
+ if (camera != null) {
+ camera!!.stopPreview()
+ camera!!.setPreviewCallbackWithBuffer(null)
+ try {
+ if (usingSurfaceTexture) {
+ camera!!.setPreviewTexture(null)
+ } else {
+ camera!!.setPreviewDisplay(null)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to clear camera preview: $e")
+ }
+
+ camera!!.release()
+ camera = null
+ }
+
+ // Release the reference to any image buffers, since these will no longer be in use.
+ bytesToByteBuffer.clear()
+ }
+
+ /** Changes the facing of the camera. */
+ @Synchronized
+ fun setFacing(facing: Int) {
+ if (facing != CAMERA_FACING_BACK && facing != CAMERA_FACING_FRONT) {
+ throw IllegalArgumentException("Invalid camera: $facing")
+ }
+ this.cameraFacing = facing
+ }
+
+ /**
+ * Opens the camera and applies the user settings.
+ *
+ * @throws IOException if camera cannot be found or preview cannot be processed
+ */
+ @SuppressLint("InlinedApi")
+ @Throws(IOException::class)
+ private fun createCamera(): Camera {
+ val requestedCameraId = getIdForRequestedCamera(cameraFacing)
+ if (requestedCameraId == -1) {
+ throw IOException("Could not find requested camera.")
+ }
+ val camera = Camera.open(requestedCameraId)
+
+ val sizePair = selectSizePair(camera, requestedPreviewWidth, requestedPreviewHeight)
+ ?: throw IOException("Could not find suitable preview size.")
+ val pictureSize = sizePair.pictureSize()
+ previewSize = sizePair.previewSize()
+
+ val previewFpsRange = selectPreviewFpsRange(camera, requestedFps)
+ ?: throw IOException("Could not find suitable preview frames per second range.")
+
+ val parameters = camera.parameters
+
+ if (pictureSize != null) {
+ parameters.setPictureSize(pictureSize.width, pictureSize.height)
+ }
+ parameters.setPreviewSize(previewSize!!.width, previewSize!!.height)
+ parameters.setPreviewFpsRange(
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
+ )
+ parameters.previewFormat = ImageFormat.NV21
+
+ setRotation(camera, parameters, requestedCameraId)
+
+ if (requestedAutoFocus) {
+ if (parameters
+ .supportedFocusModes
+ .contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)
+ ) {
+ parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
+ } else {
+ Log.i(TAG, "Camera auto focus is not supported on this device.")
+ }
+ }
+
+ camera.parameters = parameters
+
+ // Four frame buffers are needed for working with the camera:
+ //
+ // one for the frame that is currently being executed upon in doing detection
+ // one for the next pending frame to process immediately upon completing detection
+ // two for the frames that the camera uses to populate future preview images
+ //
+ // Through trial and error it appears that two free buffers, in addition to the two buffers
+ // used in this code, are needed for the camera to work properly. Perhaps the camera has
+ // one thread for acquiring images, and another thread for calling into user code. If only
+ // three buffers are used, then the camera will spew thousands of warning messages when
+ // detection takes a non-trivial amount of time.
+ camera.setPreviewCallbackWithBuffer(CameraPreviewCallback())
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
+ camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
+
+ return camera
+ }
+
+ /**
+ * Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted
+ * preview images on some devices, the picture size must be set to a size that is the same aspect
+ * ratio as the preview size or the preview may end up being distorted. If the picture size is
+ * null, then there is no picture size with the same aspect ratio as the preview size.
+ */
+ private class SizePair internal constructor(
+ previewSize: android.hardware.Camera.Size,
+ @Nullable pictureSize: android.hardware.Camera.Size?
+ ) {
+ private val preview: Size
+ private var picture: Size? = null
+
+ init {
+ preview = Size(previewSize.width, previewSize.height)
+ if (pictureSize != null) {
+ picture = Size(pictureSize.width, pictureSize.height)
+ }
+ }
+
+ internal fun previewSize(): Size {
+ return preview
+ }
+
+ @Nullable
+ internal fun pictureSize(): Size? {
+ return picture
+ }
+ }
+
+ /**
+ * Calculates the correct rotation for the given camera id and sets the rotation in the
+ * parameters. It also sets the camera's display orientation and rotation.
+ *
+ * @param parameters the camera parameters for which to set the rotation
+ * @param cameraId the camera id to set rotation based on
+ */
+ private fun setRotation(camera: Camera, parameters: Camera.Parameters, cameraId: Int) {
+ val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ var degrees = 0
+ val rotation = windowManager.defaultDisplay.rotation
+ when (rotation) {
+ Surface.ROTATION_0 -> degrees = 0
+ Surface.ROTATION_90 -> degrees = 90
+ Surface.ROTATION_180 -> degrees = 180
+ Surface.ROTATION_270 -> degrees = 270
+ else -> Log.e(TAG, "Bad rotation value: $rotation")
+ }
+
+ val cameraInfo = Camera.CameraInfo()
+ Camera.getCameraInfo(cameraId, cameraInfo)
+
+ val angle: Int
+ val displayAngle: Int
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ angle = (cameraInfo.orientation + degrees) % 360
+ displayAngle = (360 - angle) % 360 // compensate for it being mirrored
+ } else { // back-facing
+ angle = (cameraInfo.orientation - degrees + 360) % 360
+ displayAngle = angle
+ }
+
+ // This corresponds to the rotation constants.
+ this.rotation = angle / 90
+
+ camera.setDisplayOrientation(displayAngle)
+ parameters.setRotation(angle)
+ }
+
+ /**
+ * Creates one buffer for the camera preview callback. The size of the buffer is based off of the
+ * camera preview size and the format of the camera image.
+ *
+ * @return a new preview buffer of the appropriate size for the current camera settings
+ */
+ @SuppressLint("InlinedApi")
+ private fun createPreviewBuffer(previewSize: Size): ByteArray {
+ val bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21)
+ val sizeInBits = previewSize.height.toLong() * previewSize.width.toLong() * bitsPerPixel.toLong()
+ val bufferSize = Math.ceil(sizeInBits / 8.0).toInt() + 1
+
+ // Creating the byte array this way and wrapping it, as opposed to using .allocate(),
+ // should guarantee that there will be an array to work with.
+ val byteArray = ByteArray(bufferSize)
+ val buffer = ByteBuffer.wrap(byteArray)
+ if (!buffer.hasArray() || buffer.array() != byteArray) {
+ // I don't think that this will ever happen. But if it does, then we wouldn't be
+ // passing the preview content to the underlying detector later.
+ throw IllegalStateException("Failed to create valid buffer for camera source.")
+ }
+
+ bytesToByteBuffer[byteArray] = buffer
+ return byteArray
+ }
+
+ // ==============================================================================================
+ // Frame processing
+ // ==============================================================================================
+
+ /** Called when the camera has a new preview frame. */
+ private inner class CameraPreviewCallback : Camera.PreviewCallback {
+ override fun onPreviewFrame(data: ByteArray, camera: Camera) {
+ processingRunnable.setNextFrame(data, camera)
+ }
+ }
+
+ fun setMachineLearningFrameProcessor(processor: VisionImageProcessor) {
+ synchronized(processorLock) {
+ cleanScreen()
+ if (frameProcessor != null) {
+ frameProcessor!!.stop()
+ }
+ frameProcessor = processor
+ }
+ }
+
+ /**
+ * This runnable controls access to the underlying receiver, calling it to process frames when
+ * available from the camera. This is designed to run detection on frames as fast as possible
+ * (i.e., without unnecessary context switching or waiting on the next frame).
+ *
+ *
+ * While detection is running on a frame, new frames may be received from the camera. As these
+ * frames come in, the most recent frame is held onto as pending. As soon as detection and its
+ * associated processing is done for the previous frame, detection on the mostly recently received
+ * frame will immediately start on the same thread.
+ */
+ private inner class FrameProcessingRunnable internal constructor() : Runnable {
+
+ // This lock guards all of the member variables below.
+ private val lock = ReentrantLock()
+ private val condition = lock.newCondition()
+ private var active = true
+
+ // These pending variables hold the state associated with the new frame awaiting processing.
+ private var pendingFrameData: ByteBuffer? = null
+
+ /**
+ * Releases the underlying receiver. This is only safe to do after the associated thread has
+ * completed, which is managed in camera source's release method above.
+ */
+ @SuppressLint("Assert")
+ internal fun release() {
+ //assert(processingThread!!.state == Thread.State.TERMINATED)
+ }
+
+ /** Marks the runnable as active/not active. Signals any blocked threads to continue. */
+ internal fun setActive(active: Boolean) {
+ lock.withLock {
+ this.active = active
+ condition.signalAll()
+ }
+ }
+
+ /**
+ * Sets the frame data received from the camera. This adds the previous unused frame buffer (if
+ * present) back to the camera, and keeps a pending reference to the frame data for future use.
+ */
+ internal fun setNextFrame(data: ByteArray, camera: Camera) {
+ lock.withLock {
+ if (pendingFrameData != null) {
+ camera.addCallbackBuffer(pendingFrameData!!.array())
+ pendingFrameData = null
+ }
+
+ if (!bytesToByteBuffer.containsKey(data)) {
+ Log.d(
+ TAG,
+ "Skipping frame. Could not find ByteBuffer associated with the image " + "data from the camera."
+ )
+ return
+ }
+
+ pendingFrameData = bytesToByteBuffer[data]
+
+ // Notify the processor thread if it is waiting on the next frame (see below).
+ condition.signalAll()
+ }
+ }
+
+ /**
+ * As long as the processing thread is active, this executes detection on frames continuously.
+ * The next pending frame is either immediately available or hasn't been received yet. Once it
+ * is available, we transfer the frame info to local variables and run detection on that frame.
+ * It immediately loops back for the next frame without pausing.
+ *
+ *
+ * If detection takes longer than the time in between new frames from the camera, this will
+ * mean that this loop will run without ever waiting on a frame, avoiding any context switching
+ * or frame acquisition time latency.
+ *
+ *
+ * If you find that this is using more CPU than you'd like, you should probably decrease the
+ * FPS setting above to allow for some idle time in between frames.
+ */
+ @SuppressLint("InlinedApi")
+ override fun run() {
+ var data: ByteBuffer? = null
+
+ while (true) {
+ lock.withLock {
+ while (active && pendingFrameData == null) {
+ try {
+ // Wait for the next frame to be received from the camera, since we
+ // don't have it yet.
+ condition.await()
+ } catch (e: InterruptedException) {
+ Log.d(TAG, "Frame processing loop terminated.", e)
+ return
+ }
+
+ }
+
+ if (!active) {
+ // Exit the loop once this camera source is stopped or released. We check
+ // this here, immediately after the wait() above, to handle the case where
+ // setActive(false) had been called, triggering the termination of this
+ // loop.
+ return
+ }
+
+ // Hold onto the frame data locally, so that we can use this for detection
+ // below. We need to clear pendingFrameData to ensure that this buffer isn't
+ // recycled back to the camera before we are done using that data.
+ data = pendingFrameData!!
+ pendingFrameData = null
+ }
+
+ // The code below needs to run outside of synchronization, because this will allow
+ // the camera to add pending frame(s) while we are running detection on the current
+ // frame.
+
+ try {
+ synchronized(processorLock) {
+ Log.d(TAG, "Process an image")
+ frameProcessor!!.process(
+ data!!,
+ FrameMetadata.Builder()
+ .setWidth(previewSize!!.width)
+ .setHeight(previewSize!!.height)
+ .setRotation(rotation)
+ .setCameraFacing(cameraFacing)
+ .build(),
+ graphicOverlay
+ )
+ }
+ } catch (t: Throwable) {
+ Log.e(TAG, "Exception thrown from receiver.", t)
+ } finally {
+ camera!!.addCallbackBuffer(data!!.array())
+ }
+ }
+ }
+ }
+
+ /** Cleans up graphicOverlay and child classes can do their cleanups as well . */
+ private fun cleanScreen() {
+ graphicOverlay.clear()
+ }
+
+ companion object {
+ @SuppressLint("InlinedApi")
+ val CAMERA_FACING_BACK = Camera.CameraInfo.CAMERA_FACING_BACK
+
+ @SuppressLint("InlinedApi")
+ val CAMERA_FACING_FRONT = Camera.CameraInfo.CAMERA_FACING_FRONT
+
+ private val TAG = "MIDemoApp:CameraSource"
+
+ /**
+ * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL context,
+ * we can choose any ID we want here. The dummy surface texture is not a crazy hack - it is
+ * actually how the camera team recommends using the camera without a preview.
+ */
+ private val DUMMY_TEXTURE_NAME = 100
+
+ /**
+ * If the absolute difference between a preview size aspect ratio and a picture size aspect ratio
+ * is less than this tolerance, they are considered to be the same aspect ratio.
+ */
+ private val ASPECT_RATIO_TOLERANCE = 0.01f
+
+ /**
+ * Gets the id for the camera specified by the direction it is facing. Returns -1 if no such
+ * camera was found.
+ *
+ * @param facing the desired camera (front-facing or rear-facing)
+ */
+ private fun getIdForRequestedCamera(facing: Int): Int {
+ val cameraInfo = Camera.CameraInfo()
+ for (i in 0 until Camera.getNumberOfCameras()) {
+ Camera.getCameraInfo(i, cameraInfo)
+ if (cameraInfo.facing == facing) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * Selects the most suitable preview and picture size, given the desired width and height.
+ *
+ *
+ * Even though we only need to find the preview size, it's necessary to find both the preview
+ * size and the picture size of the camera together, because these need to have the same aspect
+ * ratio. On some hardware, if you would only set the preview size, you will get a distorted
+ * image.
+ *
+ * @param camera the camera to select a preview size from
+ * @param desiredWidth the desired width of the camera preview frames
+ * @param desiredHeight the desired height of the camera preview frames
+ * @return the selected preview and picture size pair
+ */
+ private fun selectSizePair(camera: Camera, desiredWidth: Int, desiredHeight: Int): SizePair? {
+ val validPreviewSizes = generateValidPreviewSizeList(camera)
+
+ // The method for selecting the best size is to minimize the sum of the differences between
+ // the desired values and the actual values for width and height. This is certainly not the
+ // only way to select the best size, but it provides a decent tradeoff between using the
+ // closest aspect ratio vs. using the closest pixel area.
+ var selectedPair: SizePair? = null
+ var minDiff = Integer.MAX_VALUE
+ for (sizePair in validPreviewSizes) {
+ val size = sizePair.previewSize()
+ val diff = Math.abs(size.width - desiredWidth) + Math.abs(size.height - desiredHeight)
+ if (diff < minDiff) {
+ selectedPair = sizePair
+ minDiff = diff
+ }
+ }
+
+ return selectedPair
+ }
+
+ /**
+ * Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is not
+ * a corresponding picture size of the same aspect ratio. If there is a corresponding picture size
+ * of the same aspect ratio, the picture size is paired up with the preview size.
+ *
+ *
+ * This is necessary because even if we don't use still pictures, the still picture size must
+ * be set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the
+ * preview images may be distorted on some devices.
+ */
+ private fun generateValidPreviewSizeList(camera: Camera): List {
+ val parameters = camera.parameters
+ val supportedPreviewSizes = parameters.supportedPreviewSizes
+ val supportedPictureSizes = parameters.supportedPictureSizes
+ val validPreviewSizes = ArrayList()
+ for (previewSize in supportedPreviewSizes) {
+ val previewAspectRatio = previewSize.width.toFloat() / previewSize.height.toFloat()
+
+ // By looping through the picture sizes in order, we favor the higher resolutions.
+ // We choose the highest resolution in order to support taking the full resolution
+ // picture later.
+ for (pictureSize in supportedPictureSizes) {
+ val pictureAspectRatio = pictureSize.width.toFloat() / pictureSize.height.toFloat()
+ if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
+ validPreviewSizes.add(SizePair(previewSize, pictureSize))
+ break
+ }
+ }
+ }
+
+ // If there are no picture sizes with the same aspect ratio as any preview sizes, allow all
+ // of the preview sizes and hope that the camera can handle it. Probably unlikely, but we
+ // still account for it.
+ if (validPreviewSizes.size == 0) {
+ Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size")
+ for (previewSize in supportedPreviewSizes) {
+ // The null picture size will let us know that we shouldn't set a picture size.
+ validPreviewSizes.add(SizePair(previewSize, null))
+ }
+ }
+
+ return validPreviewSizes
+ }
+
+ /**
+ * Selects the most suitable preview frames per second range, given the desired frames per second.
+ *
+ * @param camera the camera to select a frames per second range from
+ * @param desiredPreviewFps the desired frames per second for the camera preview frames
+ * @return the selected preview frames per second range
+ */
+ @SuppressLint("InlinedApi")
+ private fun selectPreviewFpsRange(camera: Camera, desiredPreviewFps: Float): IntArray? {
+ // The camera API uses integers scaled by a factor of 1000 instead of floating-point frame
+ // rates.
+ val desiredPreviewFpsScaled = (desiredPreviewFps * 1000.0f).toInt()
+
+ // The method for selecting the best range is to minimize the sum of the differences between
+ // the desired value and the upper and lower bounds of the range. This may select a range
+ // that the desired value is outside of, but this is often preferred. For example, if the
+ // desired frame rate is 29.97, the range (30, 30) is probably more desirable than the
+ // range (15, 30).
+ var selectedFpsRange: IntArray? = null
+ var minDiff = Integer.MAX_VALUE
+ val previewFpsRangeList = camera.parameters.supportedPreviewFpsRange
+ for (range in previewFpsRangeList) {
+ val deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]
+ val deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
+ val diff = Math.abs(deltaMin) + Math.abs(deltaMax)
+ if (diff < minDiff) {
+ selectedFpsRange = range
+ minDiff = diff
+ }
+ }
+ return selectedFpsRange
+ }
+ }
+}
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSourcePreview.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSourcePreview.kt
new file mode 100644
index 0000000..791becf
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/CameraSourcePreview.kt
@@ -0,0 +1,185 @@
+package cash.z.android.wallet.ui.util.vision
+
+// Copyright 2019 Electric Coin Company
+//
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// This file has been modified by Electric Coin Company to all for
+// better fitting previews.
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Configuration
+import android.util.AttributeSet
+import android.util.Log
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.ViewGroup
+import java.io.IOException
+
+
+/** Preview the camera image in the screen. */
+class CameraSourcePreview : ViewGroup {
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ startRequested = false
+ surfaceAvailable = false
+ surfaceView = SurfaceView(context)
+ surfaceView.holder.addCallback(SurfaceCallback())
+ addView(surfaceView)
+ }
+
+ private val surfaceView: SurfaceView
+ private var startRequested: Boolean = false
+ private var surfaceAvailable: Boolean = false
+ private var cameraSource: CameraSource? = null
+
+ private var overlay: GraphicOverlay? = null
+
+ private val isPortraitMode: Boolean
+ get() {
+ val orientation = context.resources.configuration.orientation
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ return false
+ }
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ return true
+ }
+
+ Log.d(TAG, "isPortraitMode returning false by default")
+ return false
+ }
+
+ @Throws(IOException::class)
+ fun start(cameraSource: CameraSource?) {
+ if (cameraSource == null) {
+ stop()
+ }
+
+ this.cameraSource = cameraSource
+
+ if (this.cameraSource != null) {
+ startRequested = true
+ startIfReady()
+ }
+ }
+
+ @Throws(IOException::class)
+ fun start(cameraSource: CameraSource, overlay: GraphicOverlay) {
+ this.overlay = overlay
+ start(cameraSource)
+ }
+
+ fun stop() {
+ if (cameraSource != null) {
+ cameraSource!!.stop()
+ }
+ }
+
+ fun release() {
+ if (cameraSource != null) {
+ cameraSource!!.release()
+ cameraSource = null
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ @Throws(IOException::class)
+ private fun startIfReady() {
+ if (startRequested && surfaceAvailable) {
+ cameraSource!!.start()
+ if (overlay != null) {
+ val size = cameraSource!!.previewSize!!
+ val min = Math.min(size.width, size.height)
+ val max = Math.max(size.width, size.height)
+ if (isPortraitMode) {
+ // Swap width and height sizes when in portrait, since it will be rotated by
+ // 90 degrees
+ overlay!!.setCameraInfo(min, max, cameraSource!!.cameraFacing)
+ } else {
+ overlay!!.setCameraInfo(max, min, cameraSource!!.cameraFacing)
+ }
+ overlay!!.clear()
+ }
+ startRequested = false
+ }
+ }
+
+ private inner class SurfaceCallback : SurfaceHolder.Callback {
+ override fun surfaceCreated(surface: SurfaceHolder) {
+ surfaceAvailable = true
+ try {
+ startIfReady()
+ } catch (e: IOException) {
+ Log.e(TAG, "Could not start camera source.", e)
+ }
+
+ }
+
+ override fun surfaceDestroyed(surface: SurfaceHolder) {
+ surfaceAvailable = false
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ var width = 320
+ var height = 240
+ if (cameraSource != null) {
+ val size = cameraSource!!.previewSize
+ if (size != null) {
+ width = size.width
+ height = size.height
+ }
+ }
+
+ // Swap width and height sizes when in portrait, since it will be rotated 90 degrees
+ if (isPortraitMode) {
+ val tmp = width
+ width = height
+ height = tmp
+ }
+
+ val layoutWidth = right - left
+ val layoutHeight = bottom - top
+
+ // Computes height and width for potentially doing fit width.
+ var childWidth = layoutWidth
+ var childHeight = (layoutWidth.toFloat() / width.toFloat() * height).toInt()
+
+ // If height is too tall using fit width, does fit height instead.
+ if (childHeight > layoutHeight) {
+ childHeight = layoutHeight
+ childWidth = (layoutHeight.toFloat() / height.toFloat() * width).toInt()
+ }
+
+ for (i in 0 until childCount) {
+ getChildAt(i).layout(0, 0, childWidth, childHeight)
+ Log.d(TAG, "Assigned view: $i")
+ }
+
+ try {
+ startIfReady()
+ } catch (e: IOException) {
+ Log.e(TAG, "Could not start camera source.", e)
+ }
+
+ }
+
+ companion object {
+ private val TAG = "MIDemoApp:Preview"
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/FrameMetadata.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/FrameMetadata.kt
new file mode 100644
index 0000000..5e4cd41
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/FrameMetadata.kt
@@ -0,0 +1,52 @@
+package cash.z.android.wallet.ui.util.vision
+
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/** Describing a frame info. */
+class FrameMetadata private constructor(val width: Int, val height: Int, val rotation: Int, val cameraFacing: Int) {
+
+ /** Builder of [FrameMetadata]. */
+ class Builder {
+
+ private var width: Int = 0
+ private var height: Int = 0
+ private var rotation: Int = 0
+ private var cameraFacing: Int = 0
+
+ fun setWidth(width: Int): Builder {
+ this.width = width
+ return this
+ }
+
+ fun setHeight(height: Int): Builder {
+ this.height = height
+ return this
+ }
+
+ fun setRotation(rotation: Int): Builder {
+ this.rotation = rotation
+ return this
+ }
+
+ fun setCameraFacing(facing: Int): Builder {
+ cameraFacing = facing
+ return this
+ }
+
+ fun build(): FrameMetadata {
+ return FrameMetadata(width, height, rotation, cameraFacing)
+ }
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/GraphicOverlay.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/GraphicOverlay.kt
new file mode 100644
index 0000000..fb782a3
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/GraphicOverlay.kt
@@ -0,0 +1,161 @@
+package cash.z.android.wallet.ui.util.vision
+
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.view.View
+import cash.z.android.wallet.ui.util.vision.GraphicOverlay.Graphic
+import java.util.*
+
+/**
+ * A view which renders a series of custom graphics to be overlayed on top of an associated preview
+ * (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove
+ * them, triggering the appropriate drawing and invalidation within the view.
+ *
+ *
+ * Supports scaling and mirroring of the graphics relative the camera's preview properties. The
+ * idea is that detection items are expressed in terms of a preview size, but need to be scaled up
+ * to the full view size, and also mirrored in the case of the front-facing camera.
+ *
+ *
+ * Associated [Graphic] items should use the following methods to convert to view
+ * coordinates for the graphics that are drawn:
+ *
+ *
+ * 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of the
+ * supplied value from the preview scale to the view scale.
+ * 1. [Graphic.translateX] and [Graphic.translateY] adjust the
+ * coordinate from the preview's coordinate system to the view coordinate system.
+ *
+ */
+class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
+ private val lock = Any()
+ private var previewWidth: Int = 0
+ private var widthScaleFactor = 1.0f
+ private var previewHeight: Int = 0
+ private var heightScaleFactor = 1.0f
+ private val graphics = ArrayList()
+
+ /**
+ * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
+ * this and implement the [Graphic.draw] method to define the graphics element. Add
+ * instances to the overlay using [GraphicOverlay.add].
+ */
+ abstract class Graphic(private val overlay: GraphicOverlay) {
+
+ /** Returns the application context of the app. */
+ val applicationContext: Context
+ get() = overlay.context.applicationContext
+
+ /**
+ * Draw the graphic on the supplied canvas. Drawing should use the following methods to convert
+ * to view coordinates for the graphics that are drawn:
+ *
+ *
+ * 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of the
+ * supplied value from the preview scale to the view scale.
+ * 1. [Graphic.translateX] and [Graphic.translateY] adjust the
+ * coordinate from the preview's coordinate system to the view coordinate system.
+ *
+ *
+ * @param canvas drawing canvas
+ */
+ abstract fun draw(canvas: Canvas)
+
+ /**
+ * Adjusts a horizontal value of the supplied value from the preview scale to the view scale.
+ */
+ fun scaleX(horizontal: Float): Float {
+ return horizontal * overlay.widthScaleFactor
+ }
+
+ /** Adjusts a vertical value of the supplied value from the preview scale to the view scale. */
+ fun scaleY(vertical: Float): Float {
+ return vertical * overlay.heightScaleFactor
+ }
+
+ /**
+ * Adjusts the x coordinate from the preview's coordinate system to the view coordinate system.
+ */
+ fun translateX(x: Float): Float {
+ return scaleX(x)
+ }
+
+ /**
+ * Adjusts the y coordinate from the preview's coordinate system to the view coordinate system.
+ */
+ fun translateY(y: Float): Float {
+ return scaleY(y)
+ }
+
+ fun postInvalidate() {
+ overlay.postInvalidate()
+ }
+ }
+
+ /** Removes all graphics from the overlay. */
+ fun clear() {
+ synchronized(lock) {
+ graphics.clear()
+ }
+ postInvalidate()
+ }
+
+ /** Adds a graphic to the overlay. */
+ fun add(graphic: Graphic) {
+ synchronized(lock) {
+ graphics.add(graphic)
+ }
+ }
+
+ /** Removes a graphic from the overlay. */
+ fun remove(graphic: Graphic) {
+ synchronized(lock) {
+ graphics.remove(graphic)
+ }
+ postInvalidate()
+ }
+
+ /**
+ * Sets the camera attributes for size and facing direction, which informs how to transform image
+ * coordinates later.
+ */
+ fun setCameraInfo(previewWidth: Int, previewHeight: Int, facing: Int) {
+ synchronized(lock) {
+ this.previewWidth = previewWidth
+ this.previewHeight = previewHeight
+ }
+ postInvalidate()
+ }
+
+ /** Draws the overlay with its associated graphic objects. */
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ synchronized(lock) {
+ if (previewWidth != 0 && previewHeight != 0) {
+ widthScaleFactor = canvas.width.toFloat() / previewWidth.toFloat()
+ heightScaleFactor = canvas.height.toFloat() / previewHeight.toFloat()
+ }
+
+ for (graphic in graphics) {
+ graphic.draw(canvas)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisionProcessorBase.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisionProcessorBase.kt
new file mode 100644
index 0000000..271e3fb
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisionProcessorBase.kt
@@ -0,0 +1,125 @@
+package cash.z.android.wallet.ui.util
+
+import android.graphics.Bitmap
+import androidx.annotation.GuardedBy
+import cash.z.android.wallet.ui.util.vision.BitmapUtils
+import cash.z.android.wallet.ui.util.vision.FrameMetadata
+import cash.z.android.wallet.ui.util.vision.GraphicOverlay
+import com.google.android.gms.tasks.Task
+import com.google.firebase.ml.vision.common.FirebaseVisionImage
+import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
+import java.nio.ByteBuffer
+
+/**
+ * Abstract base class for ML Kit frame processors. Subclasses need to implement {@link
+ * #onSuccess(T, FrameMetadata, GraphicOverlay)} to define what they want to with the detection
+ * results and {@link #detectInImage(FirebaseVisionImage)} to specify the detector object.
+ *
+ * @param The type of the detected feature.
+ */
+abstract class VisionProcessorBase : VisionImageProcessor {
+
+ // To keep the latest images and its metadata.
+ @GuardedBy("this")
+ private var latestImage: ByteBuffer? = null
+
+ @GuardedBy("this")
+ private var latestImageMetaData: FrameMetadata? = null
+
+ // To keep the images and metadata in process.
+ @GuardedBy("this")
+ private var processingImage: ByteBuffer? = null
+
+ @GuardedBy("this")
+ private var processingMetaData: FrameMetadata? = null
+
+ @Synchronized
+ override fun process(
+ data: ByteBuffer,
+ frameMetadata: FrameMetadata,
+ graphicOverlay: GraphicOverlay
+ ) {
+ latestImage = data
+ latestImageMetaData = frameMetadata
+ if (processingImage == null && processingMetaData == null) {
+ processLatestImage(graphicOverlay)
+ }
+ }
+
+ // Bitmap version
+ override fun process(bitmap: Bitmap, graphicOverlay: GraphicOverlay) {
+ detectInVisionImage(
+ null, /* bitmap */
+ FirebaseVisionImage.fromBitmap(bitmap),
+ null,
+ graphicOverlay
+ )
+ }
+
+ @Synchronized
+ private fun processLatestImage(graphicOverlay: GraphicOverlay) {
+ processingImage = latestImage
+ processingMetaData = latestImageMetaData
+ latestImage = null
+ latestImageMetaData = null
+ if (processingImage != null && processingMetaData != null) {
+ processImage(processingImage!!, processingMetaData!!, graphicOverlay)
+ }
+ }
+
+ private fun processImage(
+ data: ByteBuffer,
+ frameMetadata: FrameMetadata,
+ graphicOverlay: GraphicOverlay
+ ) {
+ val metadata = FirebaseVisionImageMetadata.Builder()
+ .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
+ .setWidth(frameMetadata.width)
+ .setHeight(frameMetadata.height)
+ .setRotation(frameMetadata.rotation)
+ .build()
+
+ val bitmap = BitmapUtils.getBitmap(data, frameMetadata)
+ detectInVisionImage(
+ bitmap, FirebaseVisionImage.fromByteBuffer(data, metadata), frameMetadata,
+ graphicOverlay
+ )
+ }
+
+ private fun detectInVisionImage(
+ originalCameraImage: Bitmap?,
+ image: FirebaseVisionImage,
+ metadata: FrameMetadata?,
+ graphicOverlay: GraphicOverlay
+ ) {
+ detectInImage(image)
+ .addOnSuccessListener { results ->
+ onSuccess(
+ originalCameraImage, results,
+ metadata!!,
+ graphicOverlay
+ )
+ processLatestImage(graphicOverlay)
+ }
+ .addOnFailureListener { e -> onFailure(e) }
+ }
+
+ override fun stop() {}
+
+ protected abstract fun detectInImage(image: FirebaseVisionImage): Task
+
+ /**
+ * Callback that executes with a successful detection result.
+ *
+ * @param originalCameraImage hold the original image from camera, used to draw the background
+ * image.
+ */
+ protected abstract fun onSuccess(
+ originalCameraImage: Bitmap?,
+ results: T,
+ frameMetadata: FrameMetadata,
+ graphicOverlay: GraphicOverlay
+ )
+
+ protected abstract fun onFailure(e: Exception)
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisonImageProcessor.kt b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisonImageProcessor.kt
new file mode 100644
index 0000000..00860c9
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/vision/VisonImageProcessor.kt
@@ -0,0 +1,36 @@
+package cash.z.android.wallet.ui.util
+
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+import android.graphics.Bitmap
+import cash.z.android.wallet.ui.util.vision.FrameMetadata
+import cash.z.android.wallet.ui.util.vision.GraphicOverlay
+import com.google.firebase.ml.common.FirebaseMLException
+import java.nio.ByteBuffer
+
+/** An inferface to process the images with different ML Kit detectors and custom image models. */
+interface VisionImageProcessor {
+
+ /** Processes the images with the underlying machine learning models. */
+ @Throws(FirebaseMLException::class)
+ fun process(data: ByteBuffer, frameMetadata: FrameMetadata, graphicOverlay: GraphicOverlay)
+
+ /** Processes the bitmap images. */
+ fun process(bitmap: Bitmap, graphicOverlay: GraphicOverlay)
+
+ /** Stops the underlying machine learning model and release resources. */
+ fun stop()
+}
\ No newline at end of file
diff --git a/zcash-android-wallet-app/qrecycler/src/main/res/layout/texture_view.xml b/zcash-android-wallet-app/qrecycler/src/main/res/layout/texture_view.xml
new file mode 100644
index 0000000..91953b7
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/res/layout/texture_view.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/zcash-android-wallet-app/qrecycler/src/main/res/values/attrs.xml b/zcash-android-wallet-app/qrecycler/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..7ac245d
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/res/values/attrs.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/zcash-android-wallet-app/qrecycler/src/main/res/values/public.xml b/zcash-android-wallet-app/qrecycler/src/main/res/values/public.xml
new file mode 100644
index 0000000..84966fa
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/res/values/public.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/zcash-android-wallet-app/qrecycler/src/main/res/values/styles.xml b/zcash-android-wallet-app/qrecycler/src/main/res/values/styles.xml
new file mode 100644
index 0000000..6ded716
--- /dev/null
+++ b/zcash-android-wallet-app/qrecycler/src/main/res/values/styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+